From 87309628fc2b7d83730b0522975b899c0cd685bb Mon Sep 17 00:00:00 2001 From: caiyu <290198250@qq.com> Date: Sat, 22 Feb 2025 18:13:51 +0800 Subject: [PATCH] first commit --- .gitattributes | 2 + Bandscope.ino | 677 ++++ IFsweep.ino | 495 +++ LICENSE | 674 ++++ NS2009.cpp | 124 + README.md | 28 + RXsweep.ino | 551 +++ SigLo.ino | 334 ++ SweepHi.ino | 394 ++ SweepLo.ino | 736 ++++ TFT_eSPI Setup Files/User_Setup_Select.h | 139 + .../WA2FZW_Setup_ILI9341_TinySA.h | 37 + TFT_eSPI Setup Files/simpleSA_Setup_ILI9341.h | 37 + cmd.cpp | 3520 +++++++++++++++++ cmd.h | 239 ++ data/NotoSansBold56.vlw | Bin 0 -> 100463 bytes data/NotoSansSCM14.vlw | Bin 0 -> 26168 bytes data/about.html | 63 + data/backupRestore.html | 215 + data/canvasjs.min.js | 911 +++++ data/getSweep.json | 10 + data/help.html | 115 + data/index.html | 1788 +++++++++ data/index2.html | 1264 ++++++ data/jquery-3.2.1.min.js | 12 + data/refresh.png | Bin 0 -> 655 bytes data/settings.html | 200 + data/styles.css | 333 ++ data/support.js | 101 + marker.cpp | 304 ++ marker.h | 125 + menu.cpp | 136 + menu.h | 67 + my_SA.h | 323 ++ ns2009.h | 41 + pE4302.cpp | 232 ++ pE4302.h | 88 + preferencesd.cpp | 397 ++ preferencesd.h | 42 + si4432.cpp | 923 +++++ si4432.h | 252 ++ simpleSA.h | 375 ++ sketch_jan8a.ino | 1961 +++++++++ ui.cpp | 3503 ++++++++++++++++ ui.h | 32 + 45 files changed, 21800 insertions(+) create mode 100644 .gitattributes create mode 100644 Bandscope.ino create mode 100644 IFsweep.ino create mode 100644 LICENSE create mode 100644 NS2009.cpp create mode 100644 README.md create mode 100644 RXsweep.ino create mode 100644 SigLo.ino create mode 100644 SweepHi.ino create mode 100644 SweepLo.ino create mode 100644 TFT_eSPI Setup Files/User_Setup_Select.h create mode 100644 TFT_eSPI Setup Files/WA2FZW_Setup_ILI9341_TinySA.h create mode 100644 TFT_eSPI Setup Files/simpleSA_Setup_ILI9341.h create mode 100644 cmd.cpp create mode 100644 cmd.h create mode 100644 data/NotoSansBold56.vlw create mode 100644 data/NotoSansSCM14.vlw create mode 100644 data/about.html create mode 100644 data/backupRestore.html create mode 100644 data/canvasjs.min.js create mode 100644 data/getSweep.json create mode 100644 data/help.html create mode 100644 data/index.html create mode 100644 data/index2.html create mode 100644 data/jquery-3.2.1.min.js create mode 100644 data/refresh.png create mode 100644 data/settings.html create mode 100644 data/styles.css create mode 100644 data/support.js create mode 100644 marker.cpp create mode 100644 marker.h create mode 100644 menu.cpp create mode 100644 menu.h create mode 100644 my_SA.h create mode 100644 ns2009.h create mode 100644 pE4302.cpp create mode 100644 pE4302.h create mode 100644 preferencesd.cpp create mode 100644 preferencesd.h create mode 100644 si4432.cpp create mode 100644 si4432.h create mode 100644 simpleSA.h create mode 100644 sketch_jan8a.ino create mode 100644 ui.cpp create mode 100644 ui.h diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/Bandscope.ino b/Bandscope.ino new file mode 100644 index 0000000..0fb3df3 --- /dev/null +++ b/Bandscope.ino @@ -0,0 +1,677 @@ + +uint32_t colourTest; + +/* + * Initialise variables and SI4432 for the low frequency sweep + */ +void initBandscope() +{ + // set up checkerboard sizes + waterfallHeight = WATERFALL_HEIGHT; + gridHeight = SCREEN_HEIGHT - waterfallHeight - 10; + gridWidth = SCREEN_WIDTH; + yGrid = Y_GRID; // no of grid divisions + yDelta = gridHeight / yGrid; // no of points/division + xGrid = X_GRID; + xOrigin = 0; + yOrigin = 0; + displayPoints = setting.BandscopePoints; + xDelta = SCREEN_WIDTH / xGrid; + + ClearDisplay (); +/* + * Set up the "img" Sprite. This is the image for the graph. It makes for faster display + * updates and less flicker. + * + * 16 bit colour depth is faster than 8 and much faster than 4 bit! BUT - sprites + * pushed to it do not have correct colour - 8 bit and it is fine. + * + * All marker sprites are WHITE for now. + */ + tft.unloadFont(); + + img.unloadFont(); + img.deleteSprite(); + + img.setTextSize ( 1 ); + img.setColorDepth ( 16 ); + img.setAttribute ( PSRAM_ENABLE, false ); // Don't use the PSRAM on the WROVERs + img.createSprite ( 2, gridHeight + 1 ); // Only 2 columns wide + + +/* + * The "tSprite" is used for displaying the data above the scan grid - we don't use it in this mode + * The "sSprite" is for displaying the sidebar stuff, but reused here for the waterfall + */ + + tSprite.deleteSprite(); + + sSprite.deleteSprite(); + sSprite.setColorDepth (16); // we don't need 16bit but its faster + sSprite.setAttribute ( PSRAM_ENABLE, false ); // Don't use the PSRAM on the WROVERs + sSprite.createSprite ( gridWidth, waterfallHeight ); // Full width + sSprite.setScrollRect(0, 0, gridWidth, waterfallHeight, TFT_BLACK); +/* + * Create and draw the sprite for the gain scale + */ + CreateGridScale (); + + // Make sure everything will be reset + old_settingAttenuate = -1000; + old_settingPowerGrid = -1000; + old_settingMax = -1; + old_settingMin = -1; + old_startFreq = -1; + old_stopFreq = -1; + old_requiredRBW10 = -1; + old_vbw = -1; + old_settingAverage = -1; + old_settingSpur = -100; + old_bandwidth = 0; + + SetRX ( 0 ); // LO to transmit, RX to receive + + xmit.SetDrive ( setting.Drive ); // Set transmitter power level + rcvr.SetPreampGain ( setting.PreampGain ); + + sweepStartDone = false; // Make sure this initialize is only done once per sweep + initSweep = true; + + tinySA_mode = BANDSCOPE; + setting.Mode = tinySA_mode; + ResetBandscopeMenuStack(); // Put menu stack back to root level +} + + + + +/* + * This function section handles the fast bandscope sweep + * The display is split and shows a waterfall. + * The number of points is reduced, and frequency change is done using an offset to allow the + * delay time between changing frequency and taking a reading to be reduced. + * + * Frequency scheme: + * When the LO frequency is < 480MHz the LO can be adjusted +- 80kHz from the + * nominal frequency in 156.25Hz steps, ie +- 512 steps. + * Actually this is not possible! +-511 steps is + * + * + * If the LO is above 480MHz the the adjustment is +-160kHz in 312.5Hz steps. + * If the IF is 434MHz then 480MHz -> 46Mhz for the signal being analysed, so fine + * for most of the HF bands. + * + * In bandscope mode the RBW is fixed at the minimum 2.6kHz, span at 200kHz and + * there are 80 data points + * + * 200kHz -> 2.5kHz steps between each reading or 16 * 156.25Hz if in low band + * + * Start by setting the LO to the frequency for the start of the sweep plus 80kHz + * and set the offset value at -80kHz. + * At each reading increment the offset value by 16 (8 in high band). + * In this mode the delay time between reading is set at a shorter value than + * normally used by the RBW as the LO does not turn off at each change in offset, unlike + * a normal frequency change. + * When the offset value reaches +80kHz then we need to reset the LO (using normal delaytime) + * and continue until we get to the end of the sweep. + * + * Due to the limitation of not being able to do +-512, or +-16 stesp, we will use +-31 steps per frequency + * jump instead + * + */ + + + +void doBandscope() +{ +static uint32_t autoSweepStep = 0; +static uint32_t autoSweepFreq = 0; +static uint32_t autoSweepFreqStep = 0; +static unsigned long setFreqMicros; +static unsigned long nowMicros; +static unsigned long bandscopeDelay; + + +static int16_t pointMinGain; // to record minimum gain for the current display point +static int16_t pointMaxRSSI; // to record max RSSI of the samples in the current display point +static uint32_t pointMaxFreq; // record frequency where maximum occurred + +static int16_t lastMode; // Record last operating mode (sig gen, normal) + +static uint16_t minRSSI = 255; // Minimum level for the sweep +static uint16_t lastMinRSSI; // Minimum level for the previous sweep + +static bool resetAverage; // Flag to indicate a setting has changed and average valuesneeds to be reset + +static bool jsonDocInitialised = false; +static uint16_t chunkIndex; + + + /* + * If paused and at the start of a sweep then do nothing + */ + if (!sweepStartDone && paused) + return; + + +/* + * If the "sweepStartDone" flag is false or if the "initSweep" flag is true, we need + * to set things up for the sweep. + */ + + + if (( !sweepStartDone || initSweep || changedSetting ) ) + { + if ( initSweep || changedSetting ) // Something has changed, or a first start, so need to owrk out some basic things + { + sweepPoints = setting.BandscopePoints; + autoSweepFreqStep = ( setting.BandscopeSpan ) / sweepPoints; + offsetFreqIncrement = autoSweepFreqStep; // 2500 Hz for 200kHz span, 80 points per sweep + + bandwidth = rcvr.SetRBW ( setting.BandscopeRBW10, &delaytime, &bpfIndex ); // Set it in the receiver Si4432 + + //Serial.printf("set rcvr Freq get:%u, tempIF:%u\n", rcvr.GetFrequency(), tempIF); + rcvr.SetFrequency ( setting.IF_Freq + 1300 ); // Set the RX Si4432 to the IF frequency, offset a bit + + +// sweepFreqStep = autoSweepFreqStep; // Step for each reading + + if ( setting.Attenuate != old_settingAttenuate ) + { + if ( !att.SetAtten ( setting.Attenuate )) // Set the internal attenuator + setting.Attenuate = att.GetAtten (); // Read back if limited (setting.Attenuate was outside range) + old_settingAttenuate = setting.Attenuate; + } + + // pre-calculate adjustment for RSSI values + dBadjust = (double)setting.Attenuate - 120.0 + setting.LevelOffset - setting.ExternalGain + bpfCalibrations[bpfIndex]; + Serial.printf("Bandscope dBadjust = %f; leveloffset = %f; attenuate = %i, ext gain = %f\n", + dBadjust, setting.LevelOffset, setting.Attenuate, setting.ExternalGain); + + resetAverage = changedSetting; + + maxGrid = setting.BandscopeMaxGrid; + minGrid = setting.BandscopeMinGrid; + + +#if USE_WIFI + // Vary number of points to send in each chunk depending on delaytime + // A chunk is sent at the end of each sweep regardless + wiFiPoints = wiFiTargetTime / delaytime; + if (wiFiPoints > MAX_WIFI_POINTS) + wiFiPoints = MAX_WIFI_POINTS; + if (wiFiPoints > setting.BandscopePoints) + wiFiPoints = setting.BandscopePoints; +// Serial.printf("No of wifiPoints set to %i\n", wiFiPoints); + + if ( numberOfWebsocketClients > 0 ) + pushBandscopeSettings (); +#endif // #if USE_WIFI + + } // initSweep || changedSetting + + autoSweepStep = 0; // Set the step counter to zero + autoSweepFreq = setting.BandscopeStart; // Set the start frequency. + + while (( micros() - setFreqMicros ) < delaytime ) // Make sure enough time has elasped since previous frequency write + { + } + + resetOffsets(); + + // set the offset value in the SI4432 + xmit.SetOffset(offsetValue); + + setFreqMicros = micros(); // Store the time the frequency was changed + + // set the LO frequency (offsetFreq is -ve at start!) + uint32_t xmitFreq = setting.IF_Freq + autoSweepFreq - offsetFreq; +// Serial.printf("XmitFreq %i\n", xmitFreq); + xmit.SetFrequency ( xmitFreq ); + // delay will vary depending on whether or not the nominal frequency is changed + bandscopeDelay = delaytime; // long delay + + +#if USE_WIFI + + if ( numberOfWebsocketClients > 0 ) // Start off the json document for the scan + { + chunkIndex = 0; + initChunkSweepDoc (autoSweepStep); + Points = jsonDocument.createNestedArray ( "Points" ); // Add Points array + jsonDocInitialised = true; + } + + else + jsonDocInitialised = false; + +#endif // #if USE_WIFI + +// sweepStep = 0; + startFreq = setting.BandscopeStart + setting.IF_Freq; // Start freq for the LO + stopFreq = setting.BandscopeSpan + startFreq; // Stop freq for the LO + +// Serial.printf(" start %i; stop %i; points %i \n", startFreq, stopFreq, sweepPoints ); + + lastMinRSSI = minRSSI; + minRSSI = 255; // real value should always be less + + DisplayBandscopeInfo (); // Display axis and other info + + sweepStartDone = true; // Make sure this initialize is only done once per sweep + initSweep = false; + changedSetting = false; + + lastSweepStartMicros = sweepStartMicros; // Set last time we got here + sweepStartMicros = micros(); // Current time + sweepMicros = sweepStartMicros - lastSweepStartMicros; // Calculate sweep time (no rollover handling) + + } // End of "if ( !sweepStartDone ) || initSweep || changedSetting )" + + +/* +* Here we do the actual sweep. Save the current step and frequencies for the next time +* through, then wait the required amount of time based on the RBW before taking the +* signal strength reading and changing the transmitter (LO) frequency. +*/ + + uint16_t oldSweepStep = autoSweepStep; + uint32_t oldSweepFreq = autoSweepFreq; + + /* + * Wait until time to take the next reading. If a long wait then check the touchscreen + * and Websockets while we are waiting to improve response + */ + nowMicros = micros(); + + while (( nowMicros - setFreqMicros ) < bandscopeDelay ) + { + + if ( ( nowMicros - setFreqMicros + delaytime > MIN_DELAY_WEBSOCKETS ) && + ( (nowMicros - lastWebsocketMicros > websocketInterval) || (numberOfWebsocketClients > 0) ) ) + { + lastWebsocketMicros = nowMicros; + } + if ( nowMicros - setFreqMicros > 100 ) // Wait some time to allow DMA sprite write to finish! + UiProcessTouch (); // Check the touch screen + nowMicros = micros(); + } + + int rxRSSI = rcvr.GetRSSI (); // Read the RSSI from the RX SI4432 + +/* + * Note that there are two different versions of the print statement to send the + * RSSI readings to the serial output. You can change which one is commented out. + * + * The first one produces a tab separated list of just the frequency and RSSI + * reading. That format can be easily read inte something like Excel. + * + * The second one produces a listing more fit for human consumption! + */ + + if ( showRSSI ) // Displaying RSSI? + { +// Serial.printf ( "%s\t%03d\n", +// FormatFrequency ( autoSweepFreq) , rxRSSI ); // Send it to the serial output + Serial.printf ( "Freq: %s - RSSI: %03d\n", + FormatFrequency ( autoSweepFreq) , rxRSSI ); // Send it to the serial output + } + + if ( (numberOfWebsocketClients > 0) || (setting.ShowGain) ) + gainReading = GetPreampGain ( &AGC_On, &AGC_Reg ); // Record the preamp/lna gains + + autoSweepFreq += autoSweepFreqStep; // Increment the frequency + autoSweepStep++; // and increment the step count + + offsetFreq += offsetFreqIncrement; + offsetValue += offsetIncrement; + +/* + * Change the local oscillator frequency for the next reading and record the time for + * the RBW required settling delay. + */ + + setFreqMicros = micros(); // Store the time the LO frequency was changed + + if (offsetValue >= 512) // reached offset limits + { + resetOffsets(); + xmit.SetOffset(offsetValue); + uint32_t f = setting.IF_Freq + autoSweepFreq - offsetFreq; +// Serial.printf("Sweep setFreq %i\n", f); + xmit.SetFrequency ( f ); // Set the new LO frequency as soon as RSSI read + bandscopeDelay = delaytime; + } + else + { + xmit.SetOffset(offsetValue); + bandscopeDelay = offsetDelayTime; + } + + +#if USE_WIFI + + if ( numberOfWebsocketClients > 0 ) + { + if ( jsonDocInitialised ) + { + JsonObject dataPoint = Points.createNestedObject (); // Add an object to the Json array to be pushed to the client + dataPoint["x"] = oldSweepFreq/1000000.0; // Set the x(frequency) value + dataPoint["y"] = rxRSSI; // Set the y (RSSI) value + chunkIndex++; // increment no of data points in current WiFi chunk + + if ( chunkIndex >= wiFiPoints ) // Send the chunk of data and start new jSon document + { + String wsBuffer; + + if ( wsBuffer ) + { + // Serial.print("D"); + serializeJson ( jsonDocument, wsBuffer ); + // Serial.printf("J%u", wsBuffer.length() ); + unsigned long s = millis(); + webSocket.broadcastTXT ( wsBuffer ); // Send to all connected websocket clients + if (millis() - s > 1000) + { + Serial.println("webSocketTimeout"); + Serial.println(wsBuffer); + numberOfWebsocketClients = 0; + } + // Serial.print("j"); + } + + else + Serial.println("No buffer :("); + } + } + if ( ( chunkIndex >= wiFiPoints ) || !jsonDocInitialised ) // Start new jSon document + { + chunkIndex = 0; + initChunkSweepDoc (autoSweepStep); + Points = jsonDocument.createNestedArray ( "Points" ); // Add Points array + jsonDocInitialised = true; + } + } + +#endif // #if USE_WIFI + + if (rxRSSI < minRSSI) // Detect minimum for sweep + minRSSI = rxRSSI; + + + uint16_t pixelsPerPoint = SCREEN_WIDTH / displayPoints; + for (uint16_t i = 0; i< pixelsPerPoint; i++) { + + uint16_t tmp = oldSweepStep * pixelsPerPoint + i; + + myActual[tmp] = rxRSSI; + + myGain[tmp] = gainReading; + + DrawCheckerBoard ( tmp ); // Draw the grid + + if ( resetAverage || setting.Average == AV_OFF ) // Store data, either as read or as rolling average + myData[tmp] = myActual[oldSweepStep]; + else + { + switch ( setting.Average ) + { + case AV_MIN: + if ( myData[tmp] > myActual[oldSweepStep] ) + myData[tmp] = myActual[oldSweepStep]; + break; + + case AV_MAX: + if ( myData[tmp] < myActual[oldSweepStep] ) + myData[tmp] = myActual[oldSweepStep]; + break; + + case AV_2: + myData[tmp] = ( myData[tmp] + myActual[oldSweepStep] ) / 2; + break; + + case AV_4: + myData[tmp] = ( myData[tmp]*3 + myActual[oldSweepStep] ) / 4; + break; + + case AV_8: + myData[tmp] = ( myData[tmp]*7 + myActual[oldSweepStep] ) / 8; + break; + } + DisplayPoint ( myData, tmp, AVG_COLOR ); + } + + if ( setting.ShowSweep ) + DisplayPoint ( myActual, tmp, DB_COLOR ); + + if ( setting.ShowGain ) + displayGainPoint ( myGain, tmp, GAIN_COLOR ); + + if ( setting.ShowStorage ) + DisplayPoint ( myStorage, tmp, STORAGE_COLOR ); + + // If in the first few points show the scale + if ( ( tmp < 4 * CHAR_WIDTH ) && (tmp > 0) ) + { + int16_t scaleX = -tmp + 1; // relative to the img sprite + img.setPivot( scaleX, 0); + gainScaleSprite.pushRotated ( &img, 0, TFT_BLACK ); // Send the sprite to the target sprite, with transparent colour + } + + if ( tmp > 0 ) // Only push if not first point (two pixel wide img) + img.pushSprite ( xOrigin + tmp - 1 , yOrigin ); + +/* + * put data into the top row of the waterfall + * 16 bit colours have 5 bits for Red, 6 bits for Green, 5 bits for Blue + * We will just change the green level here for first test + */ + uint32_t level = (uint32_t)( (float)(rxRSSI - setting.WaterfallMin) * setting.WaterfallGain) ; // testing colours + if (rxRSSI < setting.WaterfallMin) + level = 0; + uint32_t green = level; + uint32_t red = 0; + uint32_t blue = 0; + if (green > 63) + { + green = 63; + red = level - 63; + if ( red > 31 ) + { + red = 31; + blue = level - 63 - 31; + if ( blue > 31 ) + blue = 31; + } + } + + uint32_t pixelColour = (red << 11) + (green << 5) + blue; + + if (colourTest > 0) // delete at some stage + pixelColour = colourTest; + +// Serial.printf("rxRSSI %i; red %i; green %i; blue %i; colour %i \n", rxRSSI, red, green, blue, pixelColour); + sSprite.drawPixel ( tmp, 0, pixelColour ); + + } + +// myFreq[oldSweepStep] = oldSweepFreq; // Store the frequency for XML file creation + + + if ( autoSweepStep >= sweepPoints ) // If we have got to the end of the sweep + { +// autoSweepStep = 0; + sweepStartDone = false; + resetAverage = false; + + if ( sweepCount < 2 ) + sweepCount++; // Used to disable wifi at start + + if ( myActual[setting.BandscopePoints-1] == 0 ) // Ensure a value in last data point + { + myActual[setting.BandscopePoints-1] = rxRSSI; // Yes, save it + myGain[setting.BandscopePoints-1] = gainReading; +// myFreq[setting.BandscopePoints-1] = oldSweepFreq; + } + + if ( showRSSI == 1 ) // Only show it once? + showRSSI = 0; // Then turn it off + +#if USE_WIFI + if (( numberOfWebsocketClients > 0) && jsonDocInitialised && (chunkIndex > 0) ) + { + String wsBuffer; + + if (wsBuffer) + { + serializeJson ( jsonDocument, wsBuffer ); + webSocket.broadcastTXT ( wsBuffer ); // Send to all connected websocket clients + } + + else + Serial.println ( "No buffer :("); + } +#endif // #if USE_WIFI + + // scroll the waterfall down one pixel + sSprite.scroll( 0, 1 ); + sSprite.pushSprite( 0, gridHeight + 1 ); + + } // End of "if ( autoSweepStep >= sweepPoints )" + +} // End of "doBandscope" + + +/* + * "DisplayBandscopeInfo" - Draws the frequency info below the checkerboard. Called + * when a setting is changed to set axis labels + */ + +void DisplayBandscopeInfo () +{ +const char *averageText[] = { " OFF", " MIN", " MAX", " 2", " 4", " 8" }; +const char *referenceOutText[] = { " 30", " 15", " 10", " 4", " 3", " 2", " 1" }; + +double fStart; +double fCenter; +double fStop; + +// enum { SA_LOW_RANGE, SA_HIGH_RANGE, SIG_GEN_LOW_RANGE, SIG_GEN_HIGH_RANGE, IF_SWEEP, ZERO_SPAN_LOW_RANGE, ZERO_SPAN_HIGH_RANGE, TRACKING_GENERATOR }; + tSprite.fillSprite ( BLACK ); + tSprite.setTextColor ( WHITE ); + + +/* + * Update frequency labels at bottom if changed + */ + + tft.setTextColor ( WHITE,BLACK ); + tft.setTextSize ( 1 ); + + fStart = (double)( setting.BandscopeStart / 1000000.0 ); // Start freq + fCenter = (double)( ( setting.BandscopeStart + setting.BandscopeSpan/2.0 ) / 1000000.0 ); + fStop = (double)( (setting.BandscopeStart + setting.BandscopeSpan ) / 1000000.0 ) ; // Stop freq + + if ( old_startFreq != fStart || old_stopFreq != fStop ) + { +// Serial.printf("DisplayBandscopeInfo fStart %f; old_startFreq %f \n", fStart, old_startFreq); +// Serial.printf("DisplayBandscopeInfo fStop %f; old_stopFreq %f \n", fStop, old_stopFreq); + + tft.fillRect ( xOrigin, SCREEN_HEIGHT - + CHAR_HEIGHT, SCREEN_WIDTH - xOrigin - 1, SCREEN_HEIGHT - 1, BLACK ); + + // Show operating mode + tft.setCursor ( xOrigin + 50, SCREEN_HEIGHT - CHAR_HEIGHT ); + tft.setTextColor ( DB_COLOR ); + tft.printf ( "Mode:%s", modeText[setting.Mode] ); + tft.setTextColor ( WHITE ); + + tft.setCursor ( xOrigin + 2, SCREEN_HEIGHT - CHAR_HEIGHT ); + + tft.print ( fStart ); + + tft.setCursor ( SCREEN_WIDTH - 25, SCREEN_HEIGHT - CHAR_HEIGHT ); + tft.print ( fStop ); + + +/* + * Show the center frequency: + */ + tft.setCursor ( SCREEN_WIDTH / 2 - 20 + xOrigin, SCREEN_HEIGHT - CHAR_HEIGHT ); + tft.print ( fCenter ); + tft.print ( "(MHz)" ); + + old_startFreq = fStart; // Save current frequency range + old_stopFreq = fStop; // For next time + } + + tft.setCursor ( 220, SCREEN_HEIGHT - CHAR_HEIGHT ); // Show sweep time + tft.printf ( "%6ums", sweepMicros / 1000 ); + + +/* + * We use the "tSprite" to paint the data at the top of the screen to avoid + * flicker. + */ + + +/* + * Show marker values: + * + * The "xPos" and "yPos" arrays are the coordinates of where to place the marker data. + * + * The "posIndex" variable keeps track of the next available position for the marker + * data. If we want fixed positions for each marker, then change the "xPos" and "yPos" + * indicies to use "m". + */ + + int xPos[MARKER_COUNT] = { 20, 20, 160, 160 }; + int yPos[MARKER_COUNT] = { 0, CHAR_HEIGHT, 0, CHAR_HEIGHT }; + int posIndex = 0; + + for ( int m = 0; m < MARKER_COUNT; m++ ) + { + tSprite.setCursor ( xPos[m], yPos[m] ); + if (( marker[m].isEnabled()) && ( setting.ShowSweep || setting.Average != AV_OFF )) + { + tSprite.setTextColor ( WHITE ); + tSprite.printf ( "%u:%5.1fdBm %8.4fMHz", marker[m].Index()+1, + rssiTodBm ( oldPeaks[m].Level ), oldPeaks[m].Freq / 1000000.0 ); + } + else + { + tSprite.setTextColor ( DARKGREY ); + tSprite.printf ( "%u:", marker[m].Index()+1 ); + } + posIndex++; + } + + int x = tSprite.width () - 45; + tSprite.setTextColor ( WHITE ); + + tSprite.pushSprite ( xOrigin, 0 ); // Write sprite to the display + + updateSidebar = false; +} // End of "DisplayBandscopeInfo" + + +void resetOffsets () +{ + if (setting.BandscopeStart < 480000000) // low range. Assume never change range mid sweep! + { + offsetStep = -31; + offsetIncrement = 16; // 16 * 156.25 = 2500 + } + else // high range + { + offsetStep = -63; + offsetIncrement = 8; // 8 * 312.5 = 2500 + } + + if (setting.BandscopeSpan == 400000) // wider span, same no of points + offsetIncrement = offsetIncrement * 2; + + + offsetFreq = offsetStep * offsetFreqIncrement; + offsetValue = offsetStep * offsetIncrement; +} diff --git a/IFsweep.ino b/IFsweep.ino new file mode 100644 index 0000000..fbbf0be --- /dev/null +++ b/IFsweep.ino @@ -0,0 +1,495 @@ + +/* + * IF sweep sweeps the IF Frequency from a start value to an stop value to + * measure the SAW and RX low pass filter response. + * It requires a fixed strength and frequency signal, and this is provided + * by setting the reference output to 30MHz. + * The input should be connected to the reference signal output with an external cable + */ +void initIF_Sweep() +{ + // set up checkerboard sizes + gridHeight = GRID_HEIGHT; + gridWidth = DISPLAY_POINTS; + yGrid = Y_GRID; // no of grid divisions + yDelta = gridHeight / yGrid; // no of points/division + xGrid = X_GRID; + xOrigin = X_ORIGIN; + yOrigin = Y_ORIGIN; + displayPoints = DISPLAY_POINTS; + xDelta = displayPoints / xGrid; + + init_sweep(); + + tinySA_mode = IF_SWEEP; + setting.Mode = tinySA_mode; + + ResetIFsweepMenuStack(); // Put menu stack back to root level + +} + + +void doIF_Sweep() +{ +static uint32_t autoSweepStep = 0; +static uint32_t autoSweepFreq = 0; +static uint32_t autoSweepFreqStep = 0; +static uint32_t nextPointFreq = 0; // Frequency for the next display point. Used for substeps +static unsigned long setFreqMicros; +static unsigned long nowMicros; + +static uint32_t sweepStep; // Step count +static uint32_t sweepFreqStep; + +static int16_t pointMinGain; // to record minimum gain for the current display point +static int16_t pointMaxRSSI; // to record max RSSI of the samples in the current display point +static uint32_t pointMaxFreq; // record frequency where maximum occurred + +static int16_t lastMode; // Record last operating mode (sig gen, normal) + +static uint16_t currentPointRSSI; +static uint16_t peakRSSI; +static uint16_t prevPointRSSI; +static uint32_t peakFreq; +static uint16_t peakIndex; +static uint16_t pointsPastPeak; +static uint16_t pointsPastDip; +static uint16_t minRSSI; // Minimum level for the sweep +static uint16_t lastMinRSSI; // Minimum level for the previous sweep + +static bool jsonDocInitialised = false; +static uint16_t chunkIndex; + + /* + * If paused and at the start of a sweep then do nothing + */ + if (!sweepStartDone && paused) + return; + + + if (( !sweepStartDone || initSweep || changedSetting ) ) + { + if ( initSweep || changedSetting ) // Something has changed, or a first start, so need to work out some basic things + { + Serial.println("Init IFSweep or changedSetting"); + autoSweepFreqStep = ( stopFreq_IF - startFreq_IF ) / displayPoints; + + vbw = autoSweepFreqStep / 1000.0; // Set the video resolution + + bandwidth = rcvr.SetRBW ( 106.0, &delaytime, &bpfIndex ); // Set it in the receiver Si4432. delaytime is returned + + sweepPoints = displayPoints; // At least the right number of points for the display + + sweepFreqStep = ( stopFreq_IF - startFreq_IF ) / sweepPoints; // Step for each reading + + att.SetAtten ( 0 ); // Set the internal attenuator + + // pre-calculate adjustment for RSSI values + dBadjust = -120.0 + setting.LevelOffset - setting.ExternalGain + bpfCalibrations[bpfIndex] ; + Serial.printf("IFSweep dBadjust = %f; leveloffset = %f; attenuate = %i, ext gain = %f\n", + dBadjust, setting.LevelOffset, setting.Attenuate, setting.ExternalGain); + + xmit.SetPowerReference ( setting.ReferenceOut ); // Set the GPIO reference output + + maxGrid = setting.MaxGrid; + minGrid = setting.MinGrid; + +#if USE_WIFI + // Vary number of points to send in each chunk depending on delaytime + // A chunk is sent at the end of each sweep regardless + wiFiPoints = wiFiTargetTime / delaytime; + if (wiFiPoints > MAX_WIFI_POINTS) + wiFiPoints = MAX_WIFI_POINTS; + if (wiFiPoints > displayPoints*OVERLAP) + wiFiPoints = displayPoints*OVERLAP; +// Serial.printf("No of wifiPoints set to %i\n", wiFiPoints); + + pushIFSweepSettings(); +#endif // #if USE_WIFI + + + } // initSweep || changedSetting + + autoSweepStep = 0; // Set the step counter to zero + sweepStep = 0; + autoSweepFreq = startFreq_IF; // Set the start frequency. + + nextPointFreq = autoSweepFreq + autoSweepFreqStep; + + + while (( micros() - setFreqMicros ) < delaytime ) // Make sure enough time has elasped since previous frequency write + { + } + + //Serial.printf("set rcvr Freq get:%u, tempIF:%u\n", rcvr.GetFrequency(), tempIF); + rcvr.SetFrequency ( autoSweepFreq ); // Set the RX Si4432 to the IF frequency + + setFreqMicros = micros(); // Store the time the frequency was changed + xmit.SetFrequency ( sigFreq_IF + autoSweepFreq ); // set the LO frequency to the IF plus 30Mhz ref out + +// Serial.printf("autoSweepFreq init: %u\n", autoSweepFreq); + +#if USE_WIFI + if ( numberOfWebsocketClients > 0 ) // Start off the json document for the scan + { + chunkIndex = 0; + initChunkSweepDoc (sweepStep); + Points = jsonDocument.createNestedArray ( "Points" ); // Add Points array + jsonDocInitialised = true; + } + else + jsonDocInitialised = false; + +#endif // #if USE_WIFI + + startFreq = startFreq_IF + sigFreq_IF; // Start freq for the LO + stopFreq = stopFreq_IF + sigFreq_IF; // Stop freq for the LO + + pointMinGain = 100; // Reset min/max values + pointMaxRSSI = 0; + + DisplayInfo (); // Display axis, top and side bar text + + peakLevel = 0; // Reset the peak values for the sweep + peakFreq = 0.0; + peakGain = 100; // Set to higher than gain can ever be + + lastMinRSSI = minRSSI; + minRSSI = 300; // Higher than it can be + + +/* +* Copy the values for the peaks (marker positions) to the old versions. No need to +* reset the indicies or frequencies; just the "Level". +*/ + + for ( int i = 0; i < MARKER_COUNT; i++ ) + { + oldPeaks[i].Level = peaks[i].Level; + oldPeaks[i].Index = peaks[i].Index; + oldPeaks[i].Freq = peaks[i].Freq; + peaks[i].Level = 0; + } + + pointsPastPeak = 0; // Avoid possible peak detection at start of sweep + peakRSSI = 0; + + sweepStartDone = true; // Make sure this initialize is only done once per sweep + initSweep = false; + changedSetting = false; + + lastSweepStartMicros = sweepStartMicros; // Set last time we got here + sweepStartMicros = micros(); // Current time + sweepMicros = sweepStartMicros - lastSweepStartMicros; // Calculate sweep time (no rollover handling) + + } // End of "if ( !sweepStartDone ) || initSweep || changedSetting )" + + +/* +* Here we do the actual sweep. Save the current step and frequencies for the next time +* through, then wait the required amount of time based on the RBW before taking the +* signal strength reading and changing the transmitter (LO) frequency. +*/ + + uint16_t oldSweepStep = autoSweepStep; + uint32_t oldSweepFreq = autoSweepFreq; + + /* + * Wait until time to take the next reading. If a long wait then check the touchscreen + * and Websockets while we are waiting to improve response + */ + nowMicros = micros(); + + while (( nowMicros - setFreqMicros ) < delaytime ) + { + + if ( ( nowMicros - setFreqMicros + delaytime > MIN_DELAY_WEBSOCKETS ) && + ( (nowMicros - lastWebsocketMicros > websocketInterval) || (numberOfWebsocketClients > 0) ) ) + { +// Serial.print("W"); + // webSocket.loop (); // Check websockets - includes Yield() to allow other events to run +// Serial.println("w"); + lastWebsocketMicros = nowMicros; + } + if ( nowMicros - setFreqMicros > 100 ) // Wait some time to allow DMA sprite write to finish! + UiProcessTouch (); // Check the touch screen +// Serial.println("w"); + nowMicros = micros(); + } + + int rxRSSI = rcvr.GetRSSI (); // Read the RSSI from the RX SI4432 + +/* + * Note that there are two different versions of the print statement to send the + * RSSI readings to the serial output. You can change which one is commented out. + * + * The first one produces a tab separated list of just the frequency and RSSI + * reading. That format can be easily read inte something like Excel. + * + * The second one produces a listing more fit for human consumption! + */ + + if ( showRSSI ) // Displaying RSSI? + { +// Serial.printf ( "%s\t%03d\n", +// FormatFrequency ( autoSweepFreq) , rxRSSI ); // Send it to the serial output + Serial.printf ( "Freq: %s - RSSI: %03d\n", + FormatFrequency ( autoSweepFreq) , rxRSSI ); // Send it to the serial output + } + + if ( (numberOfWebsocketClients > 0) || (setting.ShowGain) ) + gainReading = GetPreampGain ( &AGC_On, &AGC_Reg ); // Record the preamp/lna gains + + autoSweepFreq += sweepFreqStep; // Increment the frequency + sweepStep++; // and increment the step count + +// Serial.printf("autoSweepFreq: %u Step: %u\n", autoSweepFreq, sweepStep); + + +/* + * Change the transmitter frequency for the next reading and record the time for + * the RBW required settling delay. + */ + uint32_t f = sigFreq_IF + autoSweepFreq; + + setFreqMicros = micros(); // Store the time the LO frequency was changed + rcvr.SetFrequency ( autoSweepFreq ); // Set the RX Si4432 to the IF frequency + + xmit.SetFrequency ( f ); // Set the new LO frequency as soon as RSSI read +// Serial.printf("Required: %i Actual %i\n", tempIF+autoSweepFreq, xmit.GetFrequency()); + +#if USE_WIFI + + if ( numberOfWebsocketClients > 0 ) + { + if ( jsonDocInitialised ) + { + JsonObject dataPoint = Points.createNestedObject (); // Add an object to the Json array to be pushed to the client + dataPoint["x"] = oldSweepFreq/1000000.0; // Set the x(frequency) value + dataPoint["y"] = rxRSSI; // Set the y (RSSI) value + // Serial.printf ( "Add point chunkIndex %u, sweepStep %u of %u \n", chunkIndex, sweepStep, sweepPoints); + chunkIndex++; // increment no of data points in current WiFi chunk + + if ( chunkIndex >= wiFiPoints ) // Send the chunk of data and start new jSon document + { + String wsBuffer; + + if ( wsBuffer ) + { + // Serial.print("D"); + serializeJson ( jsonDocument, wsBuffer ); + // Serial.printf("J%u", wsBuffer.length() ); + unsigned long s = millis(); + webSocket.broadcastTXT ( wsBuffer ); // Send to all connected websocket clients + if (millis() - s > 1000) + { + Serial.println("webSocketTimeout"); + Serial.println(wsBuffer); + numberOfWebsocketClients = 0; + } + // Serial.print("j"); + } + + else + Serial.println("No buffer :("); + } + } + if ( ( chunkIndex >= wiFiPoints ) || !jsonDocInitialised ) // Start new jSon document + { + chunkIndex = 0; + initChunkSweepDoc (sweepStep); + Points = jsonDocument.createNestedArray ( "Points" ); // Add Points array + jsonDocInitialised = true; + } + } + +#endif // #if USE_WIFI + + if ( rxRSSI > pointMaxRSSI ) // RSSI > maximum value for this point so far? + { + myActual[autoSweepStep] = rxRSSI; // Yes, save it + pointMaxRSSI = rxRSSI; // Added by G3ZQC - Remember new maximim + pointMaxFreq = oldSweepFreq; + } + + if ( gainReading < pointMinGain ) // Gain < minimum gain for this point so far? + { + myGain[autoSweepStep] = gainReading; // Yes, save it + pointMinGain = gainReading; // Added by G3ZQC - Remember new minimum + } + + if (rxRSSI < minRSSI) // Detect minimum for sweep + minRSSI = rxRSSI; + + +/* + * Have we enough readings for this display point? If yes, so do any averaging etc, reset + * the values so peak in the frequency step is recorded and update the display. + */ + + if ( autoSweepFreq >= nextPointFreq ) + { + nextPointFreq = nextPointFreq + autoSweepFreqStep; // Next display point frequency + autoSweepStep++; // Increment the index + + pointMinGain = 100; // Reset min/max values + pointMaxRSSI = 0; + + DrawCheckerBoard ( oldSweepStep ); // Draw the grid for the point in the sweep we have just read + + DisplayPoint ( myActual, oldSweepStep, DB_COLOR ); + + if ( setting.ShowGain ) + displayGainPoint ( myGain, oldSweepStep, GAIN_COLOR ); + +/* + * Record the peak values + */ + + if ( oldSweepStep > 0) + { + if ( peakLevel < myActual[oldSweepStep] ) + { + peakIndex = oldSweepStep; + peakLevel = myActual[oldSweepStep]; + peakFreq = oldSweepFreq; + +// Serial.printf( "peakLevel set %i, index %i\n", peakLevel, oldSweepStep); +// displayPeakData (); + } + + +/* + * Save values used by peak detection. Need to save the previous value as we only + * know we have a peak once past it! + */ + + prevPointRSSI = currentPointRSSI; + currentPointRSSI = myActual[oldSweepStep]; + + +/* + * Peak point detection. Four peaks, used to position the markers + */ + if ( currentPointRSSI >= prevPointRSSI ) // Level or ascending + { + pointsPastDip ++; + if ( pointsPastDip == PAST_PEAK_LIMIT ) + { + pointsPastPeak = 0; + } + + if ( currentPointRSSI > peakRSSI ) + { + peakRSSI = currentPointRSSI; // Store values + peakFreq = oldSweepFreq; + peakIndex = oldSweepStep; + } + } + + else + { + pointsPastPeak ++; // only a true peak if value decreased for a number of consecutive points + + if ( pointsPastPeak == PAST_PEAK_LIMIT ) // We have a peak + { + pointsPastDip = 0; + +/* + * Is this peak bigger than previous ones? Only check if bigger than smallest peak so far + */ + + if ( peakRSSI > peaks[MARKER_COUNT-1].Level ) + { + for ( uint16_t p = 0; p < MARKER_COUNT; p++ ) + { + if ( peakRSSI > peaks[p].Level ) + { + for ( uint16_t n = 3; n > p; n-- ) // Shuffle lower level peaks down + memcpy ( &peaks[n], &peaks[n-1], sizeof ( peak_t )); + + peaks[p].Level = peakRSSI; // Save the peak values + peaks[p].Freq = peakFreq; + peaks[p].Index = peakIndex; + break; + } + } + } + + peakRSSI = 0; // Reset peak values ready for next peak + } // We have a peak + } // Descending + } // if (( autoSweepFreq > 1000000 ) && (oldSweepStep > 0)) + + +/* + * Draw the markers if main sweep is displayed. The markers know if they are enabled or not + * Only paint if sweep step is in range where there will be a marker + */ + + for ( int p = 0; p < MARKER_COUNT; p++ ) + { + if (( abs ( oldSweepStep - oldPeaks[p].Index ) + <= MARKER_SPRITE_HEIGHT / 2 ) && ( oldPeaks[p].Level > (lastMinRSSI + MARKER_NOISE_LIMIT) )) + + marker[p].Paint ( &img, oldPeaks[p].Index - oldSweepStep, + rssiToImgY ( oldPeaks[p].Level ) ); + } + + // If in the last few points and gain trace is displayed show the gain scale + if ( setting.ShowGain && (oldSweepStep > displayPoints - 2 * CHAR_WIDTH) ) + { + int16_t scaleX = displayPoints - 2 * CHAR_WIDTH - oldSweepStep + 1; // relative to the img sprite + img.setPivot( scaleX, 0); + gainScaleSprite.pushRotated ( &img, 0, TFT_BLACK ); // Send the sprite to the target sprite, with transparent colour + } + + if ( oldSweepStep > 0 ) // Only push if not first point (two pixel wide img) + img.pushSprite ( xOrigin+oldSweepStep-1, yOrigin ); + +// myFreq[oldSweepStep] = oldSweepFreq; // Store the frequency for XML file creation + + } // End of "if ( autoSweepFreq >= nextPointFreq )" + + if ( sweepStep >= sweepPoints ) // If we have got to the end of the sweep + { +// autoSweepStep = 0; + sweepStartDone = false; + + if ( sweepCount < 2 ) + sweepCount++; // Used to disable wifi at start + + oldPeakLevel = peakLevel; //Save value of peak level for use by the "SetPowerLevel" function + + if ( myActual[displayPoints-1] == 0 ) // Ensure a value in last data point + { + myActual[displayPoints-1] = rxRSSI; // Yes, save it + myGain[displayPoints-1] = gainReading; +// myFreq[displayPoints-1] = oldSweepFreq; + } + + if ( showRSSI == 1 ) // Only show it once? + showRSSI = 0; // Then turn it off + +#if USE_WIFI + + if (( numberOfWebsocketClients > 0) && jsonDocInitialised && (chunkIndex > 0) ) + { + String wsBuffer; + + if (wsBuffer) + { + serializeJson ( jsonDocument, wsBuffer ); + webSocket.broadcastTXT ( wsBuffer ); // Send to all connected websocket clients + } + + else + Serial.println ( "No buffer :("); + } + +#endif // #if USE_WIFI + + } // End of "if ( sweepStep >= sweepPoints )" + + +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e62ec04 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ +GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/NS2009.cpp b/NS2009.cpp new file mode 100644 index 0000000..a38338a --- /dev/null +++ b/NS2009.cpp @@ -0,0 +1,124 @@ +#include +#include +#include "NS2009.h" + +int Map_Data (int Data, int InMin, int InMax, int OutMin, int OutMax) +{ + if (Data < InMin) + Data = InMin; + if (Data > InMax) + Data = InMax; + return ((Data-InMin)*(OutMax-OutMin))/(InMax-InMin)+OutMin; +} + +NS2009::NS2009 (void) +{ + Address = DEFAULT_NS2009_ADDR; + FlipX = false; + FlipY = false; +} + +NS2009::NS2009 (unsigned char _Address) +{ + Address = _Address; + FlipX = false; + FlipY = false; +} + +NS2009::NS2009 (bool _FlipX, bool _FlipY) +{ + Address = DEFAULT_NS2009_ADDR; + FlipX = _FlipX; + FlipY = _FlipY; +} + +NS2009::NS2009 (unsigned char _Address, bool _FlipX, bool _FlipY) +{ + Address = _Address; + FlipX = _FlipX; + FlipY = _FlipY; +} + +unsigned int NS2009::ReadRegister (unsigned char Command) +{ + unsigned char ReadData[2], i=0; + Wire.beginTransmission(Address); + Wire.write(&Command, 1); + Wire.requestFrom(Address, 2); + while (Wire.available()) + ReadData[i++] = Wire.read(); + return (ReadData[0] << 4) | (ReadData[1] >> 4); + delay(10); +} + +void NS2009::Calibrate () +{ + int P1X, P1Y, P2X, P2Y; + Serial.println ("Touch corner of the screen\n\r"); + ScanBlocking (); + P1X = RawX; + P1Y = RawY; + Serial.println ("Touch registered! Touch an opposite corner!\n\r"); + while (CheckTouched ()); + + // opposite corner + ScanBlocking (); + P2X = RawX; + P2Y = RawY; + Serial.println ("Second touch registered!\n\r"); + while (CheckTouched ()); + + MinX = MIN(P1X, P2X); + MaxX = MAX(P1X, P2X); + MinY = MIN(P1Y, P2Y); + MaxY = MAX(P1Y, P2Y); +} + +void NS2009::Calibrate (int _MinX, int _MaxX, int _MinY, int _MaxY) +{ + MinX = _MinX; + MaxX = _MaxX; + MinY = _MinY; + MaxY = _MaxY; +} + +bool NS2009::CheckTouched () +{ + do + { + RawZ = ReadRegister(NS2009_READ_Z); + } + while (RawZ == 0xFFF); // sometimes the I2C reading gives a false positive by returning only ones ==> 0xFFF = 4095 + Touched = (RawZ > THRESHOLD_Z); + return Touched; +} + +void NS2009::Scan () +{ + CheckTouched (); + RawX = ReadRegister(NS2009_READ_X); + X = Map_Data (RawX, MinX, MaxX, 0, SCREEN_SIZE_X); + RawY = ReadRegister(NS2009_READ_Y); + Y = Map_Data (RawY, MinY, MaxY, 0, SCREEN_SIZE_Y); + if (FlipX) + X = SCREEN_SIZE_X - X; + if (FlipY) + Y = SCREEN_SIZE_Y - Y; +} + +void NS2009::ScanBlocking () +{ + do + { + CheckTouched (); + } + while (!Touched); + RawX = ReadRegister(NS2009_READ_X); + X = Map_Data (RawX, MinX, MaxX, 0, SCREEN_SIZE_X); + RawY = ReadRegister(NS2009_READ_Y); + Y = Map_Data (RawY, MinY, MaxY, 0, SCREEN_SIZE_Y); + if (FlipX) + X = SCREEN_SIZE_X - X; + if (FlipY) + Y = SCREEN_SIZE_Y - Y; +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..7c3b09a --- /dev/null +++ b/README.md @@ -0,0 +1,28 @@ +# simpleSA + + Simple Spectrum Analyser, based on 2 off SI4432 modules, an ADE25 mixer, + a programmable attenuator and some filters. + This version is intended for homebrew by those experienced in amateur radio or + similar techniques, or those wishing to explore these and learn the hard way! + No support is available for the code or the hardware. + + + The code was initially based on Arduino code for STM "Blue Pill" developed by + Erik PD0EK and ported to run on an ESP32 by Dave M0WID. + + + Subsequently the code has been extensively reorganised, modified and developed, + with much of the work done by John WA2FZW. + Additional features have been added, including the ability to view the + trace and change settings from web clients, and later additional modes + such as signal generator, IFSweep and the ability to control a tracking + generator. + + Glenn VK3PE has developed some boards for this version - see his website. + + Erik has since gone on to produce a commercial version called TinySA. + The commercial version has quite a lot of additional features, but does not + include the wifi features of this version, or the integrated tracking + generator. + + Dave M0WID \ No newline at end of file diff --git a/RXsweep.ino b/RXsweep.ino new file mode 100644 index 0000000..6fc5194 --- /dev/null +++ b/RXsweep.ino @@ -0,0 +1,551 @@ + +/* + * RX sweep fixes the LO and sweeps the receiver from a start value to an stop value to + * measure the RX FIR bandpass pass filter response. This eliminates any effect from the SAW + * and low pass filters before the receiver. + * It requires a fixed strength and frequency signal, and this is provided + * by setting the reference output to say 15MHz. Avoid 30Mhz due to some stray clock signal + * The input should be connected to the reference signal output with an external cable + */ +void initRX_Sweep() +{ + // set up checkerboard sizes + gridHeight = GRID_HEIGHT; + gridWidth = DISPLAY_POINTS; + yGrid = Y_GRID; // no of grid divisions + yDelta = gridHeight / yGrid; // no of points/division + xGrid = X_GRID; + xOrigin = X_ORIGIN; + yOrigin = Y_ORIGIN; + displayPoints = DISPLAY_POINTS; + xDelta = displayPoints / xGrid; + + init_sweep(); + SetRXsweepSpan (setting.Bandwidth10 * 1000); // 10 * bandwidth + + tinySA_mode = RX_SWEEP; + setting.Mode = tinySA_mode; + + ResetRXsweepMenuStack(); // Put menu stack back to root level + +} + + +void doRX_Sweep() +{ +static uint32_t autoSweepStep = 0; +static uint32_t autoSweepFreq = 0; +static uint32_t autoSweepFreqStep = 0; +static uint32_t nextPointFreq = 0; // Frequency for the next display point. Used for substeps +static unsigned long setFreqMicros; +static unsigned long nowMicros; + +static uint16_t currentRBW10; +static uint16_t bpFilterIndex; // used to loop through the SI4432 bandpass filters when calibrating + +static uint32_t sweepStep; // Step count +static uint32_t sweepFreqStep; + +static int16_t pointMinGain; // to record minimum gain for the current display point +static int16_t pointMaxRSSI; // to record max RSSI of the samples in the current display point +static uint32_t pointMaxFreq; // record frequency where maximum occurred + +static int16_t lastMode; // Record last operating mode (sig gen, normal) + +static uint16_t currentPointRSSI; +static uint16_t peakRSSI; +static uint16_t centreRSSI; // RSSI at centre of sweep (ie at IF) +static uint16_t prevPointRSSI; +static uint32_t peakFreq; +static uint16_t peakIndex; +static uint16_t pointsPastPeak; +static uint16_t pointsPastDip; +static uint16_t minRSSI; // Minimum level for the sweep +static uint16_t lastMinRSSI; // Minimum level for the previous sweep + +static bool jsonDocInitialised = false; +static uint16_t chunkIndex; + +static uint16_t bpfCalFirstSweepDone; + /* + * If paused and at the start of a sweep then do nothing + */ + if (!sweepStartDone && paused) + return; + + + if (( !sweepStartDone || initSweep || changedSetting ) ) + { + if ( initSweep || changedSetting || (bpfCalibrate && !sweepStartDone) ) // Something has changed, or a first start, so need to work out some basic things + { + //Serial.println("Init RXSweep or changedSetting"); + + if (bpfCalibrate) + { + setting.LevelOffset = 0; + if (bpfCalFirstSweepDone) + { + // max value for this bpFilter index (previous sweep) is in peaks[0].Level + // value at the IF is in myActual[displayPoints/2] + // use the average of the two values + uint16_t rssi_average = ( peaks[0].Level + myActual[displayPoints/2] ) / 2; + bpfCalibrations[bpFilterIndex] = CAL_POWER - rssiTodBm( rssi_average ); + Serial.printf("bpfCalibration: filter: %i, rbw10=%i , cal=%f, rssi av = %i\n", + bpFilterIndex, currentRBW10, bpfCalibrations[bpFilterIndex], rssi_average); + + bpFilterIndex ++; + if (bpFilterIndex >= bpfCount) // end of calibration + { + bpfCalibrate = false; + currentRBW10 = setting.Bandwidth10; + SaveBpfCalibration(); + WriteSettings(); + Serial.println("bpfCalibration done"); + } + else // not reached end of filters + { + currentRBW10 = rcvr.GetBandpassFilter10(bpFilterIndex); + } + } + else // first sweep not done + { + bpfCalFirstSweepDone = true; + currentRBW10 = rcvr.GetBandpassFilter10(bpFilterIndex); + } + } + else // not in calibrate + { + currentRBW10 = setting.Bandwidth10; + bpFilterIndex = 0; + bpfCalFirstSweepDone = false; + } + + + SetRXsweepSpan (currentRBW10 * 1000); // 10 * bandwidth - sets up stopFreq_RX and startFreq_RX + + autoSweepFreqStep = ( stopFreq_RX - startFreq_RX ) / displayPoints; + + vbw = autoSweepFreqStep / 1000.0; // Set the video resolution + +/* + * Use the RBW setting. If zero then it is meaningless, use smallest instead + */ + bandwidth = rcvr.SetRBW ( currentRBW10, &delaytime, &bpfIndex ); // Set it in the receiver Si4432. delaytime is returned + + sweepPoints = displayPoints; + + sweepFreqStep = ( stopFreq_RX - startFreq_RX ) / sweepPoints; // Step for each reading + + att.SetAtten ( 0 ); // Set the internal attenuator + + // pre-calculate adjustment for RSSI values + dBadjust = -120.0 - setting.ExternalGain; // No Level Offset correction applied for this mode + //Serial.printf("RXSweep dBadjust = %f; ext gain = %f\n", dBadjust, setting.ExternalGain); + + xmit.SetPowerReference ( setting.ReferenceOut ); // Set the GPIO reference output + + maxGrid = setting.MaxGrid; + minGrid = setting.MinGrid; + +#if USE_WIFI + // Vary number of points to send in each chunk depending on delaytime + // A chunk is sent at the end of each sweep regardless + wiFiPoints = wiFiTargetTime / delaytime; + if (wiFiPoints > MAX_WIFI_POINTS) + wiFiPoints = MAX_WIFI_POINTS; + if (wiFiPoints > displayPoints*OVERLAP) + wiFiPoints = displayPoints*OVERLAP; +// Serial.printf("No of wifiPoints set to %i\n", wiFiPoints); + + pushRXSweepSettings(); +#endif // #if USE_WIFI + + + } // initSweep || changedSetting + + autoSweepStep = 0; // Set the step counter to zero + sweepStep = 0; + autoSweepFreq = startFreq_RX; // Set the start frequency. + + nextPointFreq = autoSweepFreq + autoSweepFreqStep; + + + while (( micros() - setFreqMicros ) < delaytime ) // Make sure enough time has elasped since previous frequency write + { + } + + //Serial.printf("set rcvr Freq get:%u, tempIF:%u\n", rcvr.GetFrequency(), tempIF); + rcvr.SetFrequency ( autoSweepFreq ); // Set the RX Si4432 to the start of the sweep + + setFreqMicros = micros(); // Store the time the frequency was changed + xmit.SetFrequency ( sigFreq_RX + setting.IF_Freq ); // set the LO frequency to the IF plus reference + +// Serial.printf("autoSweepFreq init: %u\n", autoSweepFreq); + + +#if USE_WIFI + + if ( numberOfWebsocketClients > 0 ) // Start off the json document for the scan + { + chunkIndex = 0; + initChunkSweepDoc (sweepStep); + Points = jsonDocument.createNestedArray ( "Points" ); // Add Points array + jsonDocInitialised = true; + } + else + jsonDocInitialised = false; + +#endif // #if USE_WIFI + + startFreq = startFreq_RX; // Start freq for the RX + stopFreq = stopFreq_RX; // Stop freq for the RX + + pointMinGain = 100; // Reset min/max values + pointMaxRSSI = 0; + + DisplayInfo (); // Display axis, top and side bar text + + peakLevel = 0; // Reset the peak values for the sweep + peakFreq = 0.0; + peakGain = 100; // Set to higher than gain can ever be + + lastMinRSSI = minRSSI; + minRSSI = 300; // Higher than it can be + + +/* +* Copy the values for the peaks (marker positions) to the old versions. No need to +* reset the indicies or frequencies; just the "Level". +*/ + + for ( int i = 0; i < MARKER_COUNT; i++ ) + { + oldPeaks[i].Level = peaks[i].Level; + oldPeaks[i].Index = peaks[i].Index; + oldPeaks[i].Freq = peaks[i].Freq; + peaks[i].Level = 0; + } + + pointsPastPeak = 0; // Avoid possible peak detection at start of sweep + peakRSSI = 0; + + sweepStartDone = true; // Make sure this initialize is only done once per sweep + initSweep = false; + changedSetting = false; + + lastSweepStartMicros = sweepStartMicros; // Set last time we got here + sweepStartMicros = micros(); // Current time + sweepMicros = sweepStartMicros - lastSweepStartMicros; // Calculate sweep time (no rollover handling) + + } // End of "if ( !sweepStartDone ) || initSweep || changedSetting )" + + +/* +* Here we do the actual sweep. Save the current step and frequencies for the next time +* through, then wait the required amount of time based on the RBW before taking the +* signal strength reading and changing the transmitter (LO) frequency. +*/ + + uint16_t oldSweepStep = autoSweepStep; + uint32_t oldSweepFreq = autoSweepFreq; + + /* + * Wait until time to take the next reading. If a long wait then check the touchscreen + * and Websockets while we are waiting to improve response + */ + nowMicros = micros(); + + while (( nowMicros - setFreqMicros ) < delaytime ) + { + + if ( ( nowMicros - setFreqMicros + delaytime > MIN_DELAY_WEBSOCKETS ) && + ( (nowMicros - lastWebsocketMicros > websocketInterval) || (numberOfWebsocketClients > 0) ) ) + { +// Serial.print("W"); + // webSocket.loop (); // Check websockets - includes Yield() to allow other events to run +// Serial.println("w"); + lastWebsocketMicros = nowMicros; + } + if ( nowMicros - setFreqMicros > 100 ) // Wait some time to allow DMA sprite write to finish! + UiProcessTouch (); // Check the touch screen +// Serial.println("w"); + nowMicros = micros(); + } + + int rxRSSI = rcvr.GetRSSI (); // Read the RSSI from the RX SI4432 + +/* + * Note that there are two different versions of the print statement to send the + * RSSI readings to the serial output. You can change which one is commented out. + * + * The first one produces a tab separated list of just the frequency and RSSI + * reading. That format can be easily read inte something like Excel. + * + * The second one produces a listing more fit for human consumption! + */ + + if ( showRSSI ) // Displaying RSSI? + { +// Serial.printf ( "%s\t%03d\n", +// FormatFrequency ( autoSweepFreq) , rxRSSI ); // Send it to the serial output + Serial.printf ( "Freq: %s - RSSI: %03d\n", + FormatFrequency ( autoSweepFreq) , rxRSSI ); // Send it to the serial output + } + + if ( (numberOfWebsocketClients > 0) || (setting.ShowGain) ) + gainReading = GetPreampGain ( &AGC_On, &AGC_Reg ); // Record the preamp/lna gains + + autoSweepFreq += sweepFreqStep; // Increment the frequency + sweepStep++; // and increment the step count + +// Serial.printf("autoSweepFreq: %u Step: %u\n", autoSweepFreq, sweepStep); + + +/* + * Change the transmitter frequency for the next reading and record the time for + * the RBW required settling delay. + * + * Not we also set the lo here to simulate the effect on the filters in a real sweep + */ + + setFreqMicros = micros(); // Store the time the LO frequency was changed + xmit.SetFrequency ( sigFreq_RX + setting.IF_Freq ); // set the LO frequency to the IF plus reference + rcvr.SetFrequency ( autoSweepFreq ); // Set the RX Si4432 to the IF frequency + + // Serial.printf("Step: %i Required: %i Actual %i\n", sweepStep, autoSweepFreq, rcvr.GetFrequency()); + +#if USE_WIFI + + if ( numberOfWebsocketClients > 0 ) + { + if ( jsonDocInitialised ) + { + JsonObject dataPoint = Points.createNestedObject (); // Add an object to the Json array to be pushed to the client + dataPoint["x"] = oldSweepFreq/1000000.0; // Set the x(frequency) value + dataPoint["y"] = rxRSSI; // Set the y (RSSI) value + // Serial.printf ( "Add point chunkIndex %u, sweepStep %u of %u \n", chunkIndex, sweepStep, sweepPoints); + chunkIndex++; // increment no of data points in current WiFi chunk + + if ( chunkIndex >= wiFiPoints ) // Send the chunk of data and start new jSon document + { + String wsBuffer; + + if ( wsBuffer ) + { + // Serial.print("D"); + serializeJson ( jsonDocument, wsBuffer ); + // Serial.printf("J%u", wsBuffer.length() ); + unsigned long s = millis(); + webSocket.broadcastTXT ( wsBuffer ); // Send to all connected websocket clients + if (millis() - s > 1000) + { + Serial.println("webSocketTimeout"); + Serial.println(wsBuffer); + numberOfWebsocketClients = 0; + } + // Serial.print("j"); + } + + else + Serial.println("No buffer :("); + } + } + if ( ( chunkIndex >= wiFiPoints ) || !jsonDocInitialised ) // Start new jSon document + { + chunkIndex = 0; + initChunkSweepDoc (sweepStep); + Points = jsonDocument.createNestedArray ( "Points" ); // Add Points array + jsonDocInitialised = true; + } + } + +#endif // #if USE_WIFI + + if ( rxRSSI > pointMaxRSSI ) // RSSI > maximum value for this point so far? + { + myActual[autoSweepStep] = rxRSSI; // Yes, save it + pointMaxRSSI = rxRSSI; // Added by G3ZQC - Remember new maximum + pointMaxFreq = oldSweepFreq; + } + + if ( gainReading < pointMinGain ) // Gain < minimum gain for this point so far? + { + myGain[autoSweepStep] = gainReading; // Yes, save it + pointMinGain = gainReading; // Added by G3ZQC - Remember new minimum + } + + if (rxRSSI < minRSSI) // Detect minimum for sweep + minRSSI = rxRSSI; + + +/* + * Have we enough readings for this display point? If yes, so do any averaging etc, reset + * the values so peak in the frequency step is recorded and update the display. + */ + + if ( autoSweepFreq >= nextPointFreq ) + { + nextPointFreq = nextPointFreq + autoSweepFreqStep; // Next display point frequency + autoSweepStep++; // Increment the index + + pointMinGain = 100; // Reset min/max values + pointMaxRSSI = 0; + + DrawCheckerBoard ( oldSweepStep ); // Draw the grid for the point in the sweep we have just read + + DisplayPoint ( myActual, oldSweepStep, DB_COLOR ); + + if ( setting.ShowGain ) + displayGainPoint ( myGain, oldSweepStep, GAIN_COLOR ); + +/* + * Record the peak values + */ + + if ( oldSweepStep > 0) + { + if ( peakLevel < myActual[oldSweepStep] ) + { + peakIndex = oldSweepStep; + peakLevel = myActual[oldSweepStep]; + peakFreq = oldSweepFreq; + +// Serial.printf( "peakLevel set %i, index %i\n", peakLevel, oldSweepStep); +// displayPeakData (); + } + + +/* + * Save values used by peak detection. Need to save the previous value as we only + * know we have a peak once past it! + */ + + prevPointRSSI = currentPointRSSI; + currentPointRSSI = myActual[oldSweepStep]; + + +/* + * Peak point detection. Four peaks, used to position the markers + */ + if ( currentPointRSSI >= prevPointRSSI ) // Level or ascending + { + pointsPastDip ++; + if ( pointsPastDip == PAST_PEAK_LIMIT ) + { + pointsPastPeak = 0; + } + + if ( currentPointRSSI > peakRSSI ) + { + peakRSSI = currentPointRSSI; // Store values + peakFreq = oldSweepFreq; + peakIndex = oldSweepStep; + } + } + + else + { + pointsPastPeak ++; // only a true peak if value decreased for a number of consecutive points + + if ( pointsPastPeak == PAST_PEAK_LIMIT ) // We have a peak + { + pointsPastDip = 0; + +/* + * Is this peak bigger than previous ones? Only check if bigger than smallest peak so far + */ + + if ( peakRSSI > peaks[MARKER_COUNT-1].Level ) + { + for ( uint16_t p = 0; p < MARKER_COUNT; p++ ) + { + if ( peakRSSI > peaks[p].Level ) + { + for ( uint16_t n = 3; n > p; n-- ) // Shuffle lower level peaks down + memcpy ( &peaks[n], &peaks[n-1], sizeof ( peak_t )); + + peaks[p].Level = peakRSSI; // Save the peak values + peaks[p].Freq = peakFreq; + peaks[p].Index = peakIndex; + break; + } + } + } + + peakRSSI = 0; // Reset peak values ready for next peak + } // We have a peak + } // Descending + } // if (( autoSweepFreq > 1000000 ) && (oldSweepStep > 0)) + + +/* + * Draw the markers if main sweep is displayed. The markers know if they are enabled or not + * Only paint if sweep step is in range where there will be a marker + */ + + for ( int p = 0; p < MARKER_COUNT; p++ ) + { + if (( abs ( oldSweepStep - oldPeaks[p].Index ) + <= MARKER_SPRITE_HEIGHT / 2 ) && ( oldPeaks[p].Level > (lastMinRSSI + MARKER_NOISE_LIMIT) )) + + marker[p].Paint ( &img, oldPeaks[p].Index - oldSweepStep, + rssiToImgY ( oldPeaks[p].Level ) ); + } + + // If in the last few points and gain trace is displayed show the gain scale + if ( setting.ShowGain && (oldSweepStep > displayPoints - 2 * CHAR_WIDTH) ) + { + int16_t scaleX = displayPoints - 2 * CHAR_WIDTH - oldSweepStep + 1; // relative to the img sprite + img.setPivot( scaleX, 0); + gainScaleSprite.pushRotated ( &img, 0, TFT_BLACK ); // Send the sprite to the target sprite, with transparent colour + } + + if ( oldSweepStep > 0 ) // Only push if not first point (two pixel wide img) + img.pushSprite ( xOrigin+oldSweepStep-1, yOrigin ); + +// myFreq[oldSweepStep] = oldSweepFreq; // Store the frequency for XML file creation + + } // End of "if ( autoSweepFreq >= nextPointFreq )" + + if ( sweepStep >= sweepPoints ) // If we have got to the end of the sweep + { +// autoSweepStep = 0; + sweepStartDone = false; + + if ( sweepCount < 2 ) + sweepCount++; // Used to disable wifi at start + + oldPeakLevel = peakLevel; //Save value of peak level for use by the "SetPowerLevel" function + + if ( myActual[displayPoints-1] == 0 ) // Ensure a value in last data point + { + myActual[displayPoints-1] = rxRSSI; // Yes, save it + myGain[displayPoints-1] = gainReading; +// myFreq[displayPoints-1] = oldSweepFreq; + } + + if ( showRSSI == 1 ) // Only show it once? + showRSSI = 0; // Then turn it off + +#if USE_WIFI + + if (( numberOfWebsocketClients > 0) && jsonDocInitialised && (chunkIndex > 0) ) + { + String wsBuffer; + + if (wsBuffer) + { + serializeJson ( jsonDocument, wsBuffer ); + webSocket.broadcastTXT ( wsBuffer ); // Send to all connected websocket clients + } + + else + Serial.println ( "No buffer :("); + } + +#endif // #if USE_WIFI + + } // End of "if ( sweepStep >= sweepPoints )" + + +} diff --git a/SigLo.ino b/SigLo.ino new file mode 100644 index 0000000..b93375d --- /dev/null +++ b/SigLo.ino @@ -0,0 +1,334 @@ + +/* + * ######################################################################## + * + * Initialise variables and SI4432 for sig gen mode + * + * ######################################################################## + */ +void initSigLow() +{ + + // Use the TFT_eSPI buttons for now. + // This could be changed to use something similar to the main menu + + tft.fillScreen(SIG_BACKGROUND_COLOR); + + //img.unloadFont(); // Free up memory from any previous incarnation of img + img.deleteSprite(); + img.setColorDepth ( 16 ); + img.setAttribute ( PSRAM_ENABLE, false ); // Don't use the PSRAM on the WROVERs + img.createSprite ( 320, 55 ); // used for frequency display + img.loadFont(SA_FONT_LARGE); + + SetRX ( 1 ); // LO and RX both in receive until turned on by UI + + xmit.SetOffset ( 0 ); // make sure frequency offset registers are zero + rcvr.SetOffset ( 0 ); + + + int showUpDownButtons = 0; +#ifdef SHOW_FREQ_UP_DOWN_BUTTONS + showUpDownButtons = 1; +#endif + + xmit.SetDrive ( sigGenSetting.LO_Drive ); // Set Local Oscillator power level + rcvr.SetDrive ( sigGenSetting.RX_Drive ); // Set receiver SI4432 power level + + tinySA_mode = SIG_GEN_LOW_RANGE; + setting.Mode = tinySA_mode; + + tft.unloadFont(); + tft.setCursor ( xOrigin + 50, SCREEN_HEIGHT - CHAR_HEIGHT ); + tft.setTextSize(1); + tft.setTextColor ( YELLOW ); + tft.printf ( "Mode:%s", modeText[setting.Mode] ); + tft.setTextColor ( WHITE ); + + tft.loadFont(KEY_FONT); + + // draw the buttons + for (int i = 0; i < SIG_KEY_COUNT; i++) + { + if ( showUpDownButtons || ( i > 13 )) + { + key[i].initButton(&tft, + // x, y, w, h, outline, fill, text + sig_keys[i].x, + sig_keys[i].y, + sig_keys[i].width, + sig_keys[i].height, + DARKGREY, // outline colour + sig_keys[i].color, // fill + TFT_BLACK, // Text colour + "", // 10 Byte Label + 2); // font size multiplier (not used when font loaded) + + // setLabelDatum(uint16_t x_delta, uint16_t y_delta, uint8_t datum) + key[i].setLabelDatum(1, 1, MC_DATUM); + + // Draw button and specify label string + // Specifying label string here will allow more than the default 10 byte label + key[i].drawButton(false, sig_keys[i].text); + } + } + + // draw the slider to control output level + // we re-purpose the sSprite for this + + sSprite.deleteSprite(); + sSprite.setColorDepth ( 16 ); + sSprite.setAttribute ( PSRAM_ENABLE, false ); // Don't use the PSRAM on the WROVERs + sSprite.createSprite ( SLIDER_WIDTH + 2 * SLIDER_KNOB_RADIUS + 60, 2 * SLIDER_KNOB_RADIUS ); // used for slider and value + sSprite.setTextColor(TFT_ORANGE); + sSprite.loadFont(KEY_FONT); + + // Slider range will be something like -60 to -10dBm for low frequency range + // (to be changed once I have worked out what the real values should be) + // Parameter passed in are x, y and slider knob position in % + + float sPercent = (float)(sigGenSetting.Power - sigGenSetting.Calibration + ATTENUATOR_RANGE) * 100.0 + / (float)(ATTENUATOR_RANGE); + + drawSlider(SLIDER_X, SLIDER_Y, sPercent, sigGenSetting.Power, "dBm"); + + att.SetAtten ( sigGenSetting.Calibration - sigGenSetting.Power ); // set attenuator to give required output + + oldFreq = 0; // Force write of frequency on first loop + +#if USE_WIFI + if ( numberOfWebsocketClients > 0 ) + pushSigGenSettings (); +#endif + +} + + + +/* ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' + * Low frequency range signal generator + * ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' + */ + +void doSigGenLow () +{ + static uint32_t oldIF; // to store the current IF + static uint16_t oldSigGenOutputOn; + float p; // temporary variable for slider percent + static uint16_t sliderRange = ATTENUATOR_RANGE + RX_SI4432_MAX_DRIVE * 3; + + + uint16_t t_x = 0, t_y = 0; // To store the touch coordinates + + int showUpDownButtons = 0; +#ifdef SHOW_FREQ_UP_DOWN_BUTTONS + showUpDownButtons = 1; +#endif + + if (changedSetting) { + // calculate required drive and attenuator settings + // keep drive as high as possible so nasties are attenuated + uint16_t sgRXDrive = RX_SI4432_MAX_DRIVE; + uint16_t attenuation = sigGenSetting.Calibration - sigGenSetting.Power; + if (attenuation > ATTENUATOR_RANGE ) + { + int16_t diff = attenuation - ATTENUATOR_RANGE; + sgRXDrive = RX_SI4432_MAX_DRIVE - (int16_t)( (diff-1)/3 ) - 1 ; + attenuation = ATTENUATOR_RANGE - 2 + (diff-1)%3; + } + SetSGRxDrive(sgRXDrive); + att.SetAtten(attenuation); + Serial.printf("sigGenLow - rxDrive set to %i, attenuation set to %i, cal is %i\n", sgRXDrive, attenuation, sigGenSetting.Calibration); + + #if USE_WIFI + if ( numberOfWebsocketClients > 0 ) + pushSigGenSettings (); + #endif + + changedSetting = false; + } + + + // Get current touch state and coordinates + boolean pressed = tft.getTouch(&t_x, &t_y); // Just uses standard TFT_eSPI function as not bothered about speed + + // Adjust press state of each key appropriately + for (uint8_t b = 0; b < SIG_KEY_COUNT; b++) { + if (pressed && key[b].contains(t_x, t_y)) + key[b].press(true); // tell the button it is pressed + else + key[b].press(false); // tell the button it is NOT pressed + } + + // Check if any key has changed state + for (uint8_t b = 0; b < SIG_KEY_COUNT; b++) + { + if ( showUpDownButtons || ( b > 13 )) + { + if ( key[b].justPressed() ) { + switch (b) { + case 0: // Increment buttons + case 1: + case 2: + case 3: + case 4: + case 5: + case 6: + incrementFreq( pow(10, 8-b) ); + changedSetting=true; + break; + + case 7: // Decrement buttons + case 8: + case 9: + case 10: + case 11: + case 12: + case 13: + decrementFreq( pow(10, 15-b) ); + changedSetting=true; + break; + + case 14: // Return to SAlo mode + key[b].drawButton(true, sig_keys[b].activeText); + break; + + case 15: // toggle the output on/off + sigGenOutputOn = !sigGenOutputOn; + break; + + case 16: // launch menu + key[b].drawButton(true, sig_keys[b].activeText); + break; + + default: + Serial.printf("Button %i press not handled", b ); + break; + } + } + } + + + if (key[b].isPressed()) { // button held + return; + } + + +// // If button was just released + if (key[b].justReleased()) + { + switch (b) { + case 14: // Return to SAlo mode + WriteSigGenSettings (); + initSweepLow(); + break; + + case 16: // launch signal generator menu + StartSigGenMenu ( ); + return; + + case 17: // launch frequency keypad + StartSigGenFreq ( ); + return; + } + } + + // check if state changed - can change from command or from wifi + switch (b) { + case 15: // On/Off button + if (oldSigGenOutputOn != sigGenOutputOn) + { + if (sigGenOutputOn) { + SetRX(3); // both LO and RX in transmit. Output levels have been set in the init function + key[b].drawButton(true, sig_keys[b].activeText); + tft.setCursor(sig_keys[b].x + 30, sig_keys[b].y); + tft.fillRect(sig_keys[b].x + 30, sig_keys[b].y, sig_keys[b].x + 60, sig_keys[b].y + 10, SIG_BACKGROUND_COLOR); + tft.setTextColor(TFT_GREEN, SIG_BACKGROUND_COLOR ); + tft.print(" ON"); + } else { + SetRX(1); // Both in receive + key[b].drawButton(false, sig_keys[b].text); + tft.setCursor(sig_keys[b].x + 30, sig_keys[b].y); + tft.fillRect(sig_keys[b].x + 30, sig_keys[b].y, sig_keys[b].x + 60, sig_keys[b].y + 10, SIG_BACKGROUND_COLOR); + tft.setTextColor(TFT_WHITE, SIG_BACKGROUND_COLOR ); + tft.print("OFF"); + } + changedSetting = true; + } + break; + + } // on of state change switch + + + } // end of keys loop + + + + // Check if slider touched + if ( sliderPressed( pressed, t_x, t_y) ) + { + p = sliderPercent( t_x ); // position of slider in % + float pwr = p * (sliderRange)/100.0 + sigGenSetting.Calibration - sliderRange; + + drawSlider ( SLIDER_X, SLIDER_Y, p, pwr, "dBm" ); + sigGenSetting.Power = pwr; + changedSetting = true; + } else { + p = ( sigGenSetting.Power - (sigGenSetting.Calibration - sliderRange) ) * 100.0 / sliderRange; + drawSlider ( SLIDER_X, SLIDER_Y, p, sigGenSetting.Power, "dBm" ); + } + + + + // draw frequency. Uses a sprite to avoid flicker + img.fillSprite(SIG_BACKGROUND_COLOR); + img.setCursor(5,5); + img.setTextColor(TFT_ORANGE); + img.printf("%s",DisplayFrequency ( sigGenSetting.Frequency ) ); + img.pushSprite(0,80); + + + /* + * set RX to IF_Frequency and LO to IF plus required frequency + * + * but only if value has changed to avoid the SI4432 running its state change sequencer + * The mixer will produce IF + LO and IF - LO + * The Low pass filter will filter out the higher frequency and LO leakage + * The IF SAW filter will smooth out the waveform produced by the SI4432 + */ + if ( (oldFreq != sigGenSetting.Frequency) || (oldIF != setting.IF_Freq) ) + { + rcvr.SetFrequency ( setting.IF_Freq ); + xmit.SetFrequency ( setting.IF_Freq + sigGenSetting.Frequency ); + Serial.println("set frequency"); + if (sigGenOutputOn) + { + delayMicroseconds(300); + SetRX(3); // both LO and RX in tx mode + } + changedSetting = true; + } + + oldFreq = sigGenSetting.Frequency; + oldIF = setting.IF_Freq; + oldSigGenOutputOn = sigGenOutputOn; + +} + + +void incrementFreq(uint32_t amount) { + sigGenSetting.Frequency += amount; + if (sigGenSetting.Frequency > MAX_SIGLO_FREQ) + sigGenSetting.Frequency = MAX_SIGLO_FREQ; +} + +void decrementFreq(uint32_t amount) { + + if (sigGenSetting.Frequency > amount) { + sigGenSetting.Frequency -= amount; + if (sigGenSetting.Frequency < MIN_SIGLO_FREQ) + sigGenSetting.Frequency = MIN_SIGLO_FREQ; + } else { + sigGenSetting.Frequency = MIN_SIGLO_FREQ; + } +} diff --git a/SweepHi.ino b/SweepHi.ino new file mode 100644 index 0000000..2aba869 --- /dev/null +++ b/SweepHi.ino @@ -0,0 +1,394 @@ +// Version: 1.0 + +#ifdef HI_RANGE +/* + * Initialise variables and SI4432 for the high frequency sweep + */ +void initSweepHigh() +{ +#ifdef RF_SWITCH + digitalWrite (RF_SWITCH, RF_SWITCH_HIGH_RANGE); // LO RF Switch to mixer +#endif + // set up checkerboard sizes + gridHeight = GRID_HEIGHT; + gridWidth = DISPLAY_POINTS; + yGrid = Y_GRID; // no of grid divisions + yDelta = gridHeight / yGrid; // no of points/division + xGrid = X_GRID; + xOrigin = X_ORIGIN; + yOrigin = Y_ORIGIN; + displayPoints = DISPLAY_POINTS; + scaleX = displayPoints - 2 * CHAR_WIDTH; + xDelta = displayPoints / xGrid; + + startFreqMinLimit = START_MIN_HIGH; + stopFreqMaxLimit = STOP_MAX_HIGH; + + simpleSA_mode = SA_HIGH_RANGE; + setting.Mode = simpleSA_mode; + setSettingChangeMillis(); + + init_sweep(); + + SetRefOutput ( -1 ); // Turn off the ref output + + ResetSAMenuStack(); // Put menu stack back to root level + quickSelectionSelected = false; + validateQuickSelect ( 1 ); // make sure quick select is valid + + resetMarkers(); + +// Serial.printf("InitSweepHigh Done "); +// Serial.printf("lo reg 2= %x\n", xmit.ReadByte(2)); +} + + + +/* + * This function section handles the high freq range sweep + */ + +void doSweepHigh() +{ +static uint32_t autoSweepStep = 0; +static uint32_t autoSweepFreq = 0; +static uint32_t autoSweepFreqStep = 0; +static uint32_t nextPointFreq = 0; // Frequency for the next display point. Used for substeps +static unsigned long nowMicros; + +static int16_t pointMinGain; // to record minimum gain for the current display point + +static int16_t lastMode; // Record last operating mode (sig gen, normal) + +static uint32_t actualFreq; // actual frequency + + + /* + * If paused and at the start of a sweep then do nothing + */ + if (!sweepStartDone && paused) + return; + + +/* + * If the "sweepStartDone" flag is false or if the "initSweep" flag is true, we need + * to set things up for the sweep. + */ + if (( !sweepStartDone || initSweep || changedSetting ) ) + { + if ( initSweep || changedSetting ) // Something has changed, or a first start, so need to owrk out some basic things + { + //Serial.println("InitSweep or changedSetting"); + autoSweepFreqStep = ( settingHigh.ScanStop - settingHigh.ScanStart ) / displayPoints; // MHz + + vbw = autoSweepFreqStep / 1000.0; // Set the video resolution (kHz) + requiredRBW10 = settingHigh.Bandwidth10; // and the resolution bandwidth (kHz * 10) + + if ( requiredRBW10 == 0 ) // If the bandwidth is on "Auto" work out the required RBW + requiredRBW10 = (( settingHigh.ScanStop - settingHigh.ScanStart )) / 29000; // 290 points on display, kHz + + if ( requiredRBW10 < 26 ) // If it's less than 2.6KHz + requiredRBW10 = 26; // set it to 2.6KHz + + if ( requiredRBW10 > 6207 ) + requiredRBW10 = 6207; + + if ( requiredRBW10 != old_requiredRBW10 ) + { + bandwidth = xmit.SetRBW ( requiredRBW10, &delaytime, &bpfIndex ); // Set it in the receiver Si4432 + old_requiredRBW10 = requiredRBW10; + } + + + /* + * Need multiple readings for each pixel in the display to avoid missing signals. + * Work out how many points needed for the whole sweep: + */ + + sweepPoints = (uint32_t)(( settingHigh.ScanStop - settingHigh.ScanStart ) / bandwidth / 1000.0 * OVERLAP + 0.5); // allow for some overlap (filters will have 3dB roll off at edge) and round up + + if ( sweepPoints < displayPoints ) + sweepPoints = displayPoints; // At least the right number of points for the display + + sweepFreqStep = ( settingHigh.ScanStop - settingHigh.ScanStart ) / sweepPoints; // Step for each reading + + // pre-calculate adjustment for RSSI values + dBadjust = - 120.0 + settingHigh.LevelOffset - settingHigh.ExternalGain + bpfCalibrations[bpfIndex]; +// Serial.printf("SweepHi dBadjust = %f; leveloffset = %f; ext gain = %f, bpfCal = %f\n", +// dBadjust, settingHigh.LevelOffset, settingHigh.ExternalGain, bpfCalibrations[bpfIndex]); + + resetAverage = changedSetting; + + maxGrid = settingHigh.MaxGrid; + minGrid = settingHigh.MinGrid; + +#if ( USE_WIFI ) + // Vary number of points to send in each chunk depending on delaytime + // A chunk is sent at the end of each sweep regardless + wiFiPoints = wiFiTargetTime / delaytime; + if (wiFiPoints > MAX_WIFI_POINTS) + wiFiPoints = MAX_WIFI_POINTS; + if (wiFiPoints > displayPoints*OVERLAP) + wiFiPoints = displayPoints*OVERLAP; +// Serial.printf("No of wifiPoints set to %i\n", wiFiPoints); + + if ( numberOfWebsocketClients > 0 ) + pushSettings (); +#endif // #if ( USE_WIFI ) + + } // initSweep || changedSetting + + autoSweepStep = 0; // Set the step counter to zero + sweepStep = 0; + autoSweepFreq = settingHigh.ScanStart; // Set the start frequency. + nextPointFreq = autoSweepFreq + autoSweepFreqStep; + + + while (( micros() - setFreqMicros ) < delaytime ) // Make sure enough time has elasped since previous frequency write + { + } + + + xmit.SetFrequency ( autoSweepFreq ); // set the LO frequency + + setFreqMicros = micros(); // Store the time the frequency was changed + + + /* + * Actual frequency in the SI4432 is rounded and is limited by the possible resolution + */ + actualFreq = xmit.GetFrequency(); // Used for next RSSI command and JSON entry + + +#if ( USE_WIFI ) + + if ( numberOfWebsocketClients > 0 ) // Start off the json document for the scan + { + initChunkSweepDoc (sweepStep); + jsonDocInitialised = true; + } + else + jsonDocInitialised = false; + +#endif // #if ( USE_WIFI ) + + startFreq = settingHigh.ScanStart; // Start freq for the LO + stopFreq = settingHigh.ScanStop; // Stop freq for the LO + + pointMinGain = 100; // Reset min/max values + pointMaxRSSI = 0; + +/* + * If an adjustment of the level is requested to obtain correct dBm + */ + if ( setActualPowerRequested ) + { + SetPowerLevel ( actualPower ); + setActualPowerRequested = false; +// Serial.printf ( "Setting actual Power %f \n", actualPower ); + } + +/* +* Copy the values for the peaks (marker positions) to the old versions. No need to +* reset the indicies or frequencies; just the "Level". +*/ + for ( int i = 0; i < MARKER_COUNT; i++ ) + { + marker[i].SetValue(peaks[i].Index, peaks[i].Freq, peaks[i].Level); + peaks[i].Level = 0; + } + + + DisplayInfo (); // Display axis, top and side bar text + + peakLevel = 0; // Reset the peak values for the sweep + peakFreq = 0.0; + peakGain = 100; // Set to higher than gain can ever be + + lastMinRSSI = minRSSI; + minRSSI = 300; // Higher than it can be + + pointsPastPeak = 0; // Avoid possible peak detection at start of sweep + peakRSSI = 0; + maxRSSI = 0; + + sweepStartDone = true; // Make sure this initialize is only done once per sweep + initSweep = false; + changedSetting = false; + + lastSweepStartMicros = sweepStartMicros; // Set last time we got here + sweepStartMicros = micros(); // Current time + sweepMicros = sweepStartMicros - lastSweepStartMicros; // Calculate sweep time (no rollover handling) + + + + } // End of "if ( !sweepStartDone ) || initSweep || changedSetting )" + + +/* +* Here we do the actual sweep. Save the current step and frequencies for the next time +* through, then wait the required amount of time based on the RBW before taking the +* signal strength reading and changing the transmitter (LO) frequency. +*/ + + oldSweepStep = autoSweepStep; + oldSweepFreq = actualFreq; + + /* + * Wait until time to take the next reading. If a long enough wait left + * then check the touchscreen and Websockets while we are waiting + * to improve response + */ + + + nowMicros = micros(); + + while (( nowMicros - setFreqMicros ) < delaytime ) + { + +#if ( USE_WIFI ) + if ( ( nowMicros - setFreqMicros + delaytime > MIN_DELAY_WEBSOCKETS ) && + ( (nowMicros - lastWebsocketMicros > websocketInterval) || (numberOfWebsocketClients > 0) ) ) + { +// Serial.print("W"); + webSocket.loop (); // Check websockets - includes Yield() to allow other events to run +// Serial.println("w"); + lastWebsocketMicros = nowMicros; + } +#endif + + UiProcess(); // Check the touch screen and encoder + if ( ui_mode != UI_NORMAL ) + return; // avoid risk of drawing vertical stripe over menu during remainder of scan + +// Serial.println("w"); + nowMicros = micros(); + } + + rxRSSI = xmit.GetRSSI (); // Read the RSSI from the LO SI4432 + +/* + * Note that there are two different versions of the print statement to send the + * RSSI readings to the serial output. You can change which one is commented out. + * + * The first one produces a tab separated list of just the frequency and RSSI + * reading. That format can be easily read inte something like Excel. + * + * The second one produces a listing more fit for human consumption! + */ + + if ( showRSSI ) // Displaying RSSI? + { +// Serial.printf ( "%s\t%03d\n", +// FormatFrequency ( autoSweepFreq) , rxRSSI ); // Send it to the serial output + Serial.printf ( " LO Freq: %s", FormatFrequency ( xmit.GetFrequency() ) ); + Serial.printf ( " Sweep Freq: %s (%u)", FormatFrequency ( autoSweepFreq), autoSweepFreq ); + Serial.printf ( " Actual Freq %s - RSSI: %03d\n", + FormatFrequency ( actualFreq ), rxRSSI ); // Send it to the serial output + } + + if ( (numberOfWebsocketClients > 0) || (settingHigh.ShowGain) ) + gainReading = GetPreampGainHigh ( &AGC_On, &AGC_Reg ); // Record the preamp/lna gains + + autoSweepFreq += sweepFreqStep; // Increment the frequency + sweepStep++; // and increment the step count + + +/* + * Change the transmitter frequency for the next reading and record the time for + * the RBW required settling delay. + */ + + xmit.SetFrequency ( autoSweepFreq ); // Set the new LO frequency as soon as RSSI read +// Serial.printf("LO Required: %i Actual %i\n", f, xmit.GetFrequency()); + + setFreqMicros = micros(); // Store the time the LO frequency was changed + +#if ( USE_WIFI ) + addJsonDataPoint (); +#endif + + +/* + * Actual frequency in the SI4432 is rounded and is limited by the possible resolution + */ + actualFreq = xmit.GetFrequency(); // Used for next RSSI command and JSON entry + + + if ( rxRSSI > pointMaxRSSI ) // RSSI > maximum value for this point so far? + { + myActual[autoSweepStep] = rxRSSI; // Yes, save it + pointMaxRSSI = rxRSSI; // Added by G3ZQC - Remember new maximim + pointMaxFreq = oldSweepFreq; + } + + if ( gainReading < pointMinGain ) // Gain < minimum gain for this point so far? + { + myGain[autoSweepStep] = gainReading; // Yes, save it + pointMinGain = gainReading; // Added by G3ZQC - Remember new minimum + } + + if (rxRSSI < minRSSI) // Detect minimum for sweep + minRSSI = rxRSSI; + + +/* + * Have we enough readings for this display point? If yes, so do any averaging etc, reset + * the values so peak in the frequency step is recorded and update the display. + */ + + if ( autoSweepFreq >= nextPointFreq ) + { + nextPointFreq = nextPointFreq + autoSweepFreqStep; // Next display point frequency + autoSweepStep++; // Increment the index + + myFreq[oldSweepStep] = oldSweepFreq; // Store the frequency for the markers + + DrawTraces ( oldSweepStep ); // Plot the data points (pass in freq of zero so no low pass filter compensation) + + detectPeaks(); + + pointMinGain = 100; // Reset min/max values + pointMaxRSSI = 0; + + myFreq[oldSweepStep] = oldSweepFreq; // Store the frequency for the markers + + if ( settingHigh.SubtractStorage ) + rxRSSI = 128 + rxRSSI - myStorage[oldSweepStep]; + + if ( oldSweepStep > 0 ) // Only push if not first point (two pixel wide img) + img.pushSprite ( xOrigin+oldSweepStep-1, yOrigin ); + + } // End of "if ( autoSweepFreq >= nextPointFreq )" + + if ( sweepStep >= sweepPoints ) // If we have got to the end of the sweep + { +// autoSweepStep = 0; + sweepStartDone = false; + resetAverage = false; + + if ( sweepCount < 2 ) + sweepCount++; // Used to disable wifi at start + +// Serial.printf("MaxRSSI = %i, freq = %i\n", maxRSSI, maxFreq); + + oldPeakLevel = maxRSSI; //Save value of peak level for use by the "SetPowerLevel" function + + if ( myActual[displayPoints-1] == 0 ) // Ensure a value in last data point + { + myActual[displayPoints-1] = rxRSSI; // Yes, save it + myGain[displayPoints-1] = gainReading; + myFreq[displayPoints-1] = oldSweepFreq; + } + + if ( showRSSI == 1 ) // Only show it once? + showRSSI = 0; // Then turn it off + +#if ( USE_WIFI ) + sendJsonChunk(); +#endif + + } // End of "if ( sweepStep >= sweepPoints )" +} // End of "doSweepHigh" + +#endif // #ifdef HI_RANGE diff --git a/SweepLo.ino b/SweepLo.ino new file mode 100644 index 0000000..dcdcae2 --- /dev/null +++ b/SweepLo.ino @@ -0,0 +1,736 @@ + +/* + * Initialise variables and SI4432 for the low frequency sweep + */ +void initSweepLow() +{ + // set up checkerboard sizes + gridHeight = GRID_HEIGHT; + gridWidth = DISPLAY_POINTS; + yGrid = Y_GRID; // no of grid divisions + yDelta = gridHeight / yGrid; // no of points/division + xGrid = X_GRID; + xOrigin = X_ORIGIN; + yOrigin = Y_ORIGIN; + displayPoints = DISPLAY_POINTS; + xDelta = displayPoints / xGrid; + + init_sweep(); + + tinySA_mode = SA_LOW_RANGE; + setting.Mode = tinySA_mode; + + ResetSAMenuStack(); // Put menu stack back to root level + +} + + + + +/* + * This function section handles the low freq range sweep + */ + + + +void doSweepLow() +{ +static uint32_t autoSweepStep = 0; +static uint32_t autoSweepFreq = 0; +static uint32_t autoSweepFreqStep = 0; +static uint32_t nextPointFreq = 0; // Frequency for the next display point. Used for substeps +static unsigned long setFreqMicros; +static unsigned long nowMicros; + +static uint32_t sweepStep; // Step count +static uint32_t sweepFreqStep; + +static int16_t pointMinGain; // to record minimum gain for the current display point +static int16_t pointMaxRSSI; // to record max RSSI of the samples in the current display point +static uint32_t pointMaxFreq; // record frequency where maximum occurred + +static int16_t lastMode; // Record last operating mode (sig gen, normal) +static uint32_t lastIF; +static bool spurToggle; +static uint32_t actualFreq; // actual frequency +static uint32_t actualIF; // actual IF (Rx frequency) + +static uint16_t currentPointRSSI; +static uint16_t peakRSSI; +static uint16_t prevPointRSSI; +static uint32_t peakFreq; +static uint16_t peakIndex; +static uint16_t maxRSSI; +static uint32_t maxFreq; +static uint16_t maxIndex; +static uint16_t pointsPastPeak; +static uint16_t pointsPastDip; +static uint16_t minRSSI; // Minimum level for the sweep +static uint16_t lastMinRSSI; // Minimum level for the previous sweep + +static bool resetAverage; // Flag to indicate a setting has changed and average valuesneeds to be reset + +static bool jsonDocInitialised = false; +static uint16_t chunkIndex; +static uint32_t offsetIF; // IF frequency offset by half the bandwidth to position in the centre of the filter +static uint32_t tgIF; // Track gen IF - SA IF plus any offset if both SI4432 defined + + +#ifdef RF_SWITCH + digitalWrite (RF_SWITCH, 1); // LO RF Switch to mixer +#endif + /* + * If paused and at the start of a sweep then do nothing + */ + if (!sweepStartDone && paused) + return; + + +/* + * If the "sweepStartDone" flag is false or if the "initSweep" flag is true, we need + * to set things up for the sweep. + */ + + + if (( !sweepStartDone || initSweep || changedSetting ) ) + { + if ( initSweep || changedSetting ) // Something has changed, or a first start, so need to owrk out some basic things + { + //Serial.println("InitSweep or changedSetting"); + autoSweepFreqStep = ( setting.ScanStop - setting.ScanStart ) / displayPoints; + + vbw = autoSweepFreqStep / 1000.0; // Set the video resolution + requiredRBW10 = setting.Bandwidth10; // and the resolution bandwidth + + if ( requiredRBW10 == 0 ) // If the bandwidth is on "Auto" work out the required RBW + requiredRBW10 = (( setting.ScanStop - setting.ScanStart )) / 29000; // 290 points on display, kHz + + if ( requiredRBW10 < 26 ) // If it's less than 2.6KHz + requiredRBW10 = 26; // set it to 2.6KHz + + if ( requiredRBW10 > 6207 ) + requiredRBW10 = 6207; + + if ( requiredRBW10 != old_requiredRBW10 ) + { + bandwidth = rcvr.SetRBW ( requiredRBW10, &delaytime, &bpfIndex ); // Set it in the receiver Si4432 + old_requiredRBW10 = requiredRBW10; + } + +/* + * The FIR filters in the SI4432 are centred above the nominal IF frequency as that is where the signals are meant to be. + * To make sure we are not on the edge of the filters offset the IF frequency down by half the bandwidth + * This needs to be optimised ********* + */ +// offsetIF = setting.IF_Freq + setting.Bandwidth10 * 50; // bW10 is in kHz * 10, so * 100-> kHz, halved + offsetIF = setting.IF_Freq + RX_PASSBAND_OFFSET; // half of narrowest RBW + + /* + * Need multiple readings for each pixel in the display to avoid missing signals. + * Work out how many points needed for the whole sweep: + */ + + sweepPoints = (uint32_t)(( setting.ScanStop - setting.ScanStart ) / bandwidth / 1000.0 * OVERLAP + 0.5); // allow for some overlap (filters will have 3dB roll off at edge) and round up + + if ( sweepPoints < displayPoints ) + sweepPoints = displayPoints; // At least the right number of points for the display + + sweepFreqStep = ( setting.ScanStop - setting.ScanStart ) / sweepPoints; // Step for each reading + + if ( setting.Attenuate != old_settingAttenuate ) + { + if ( !att.SetAtten ( setting.Attenuate )) // Set the internal attenuator + setting.Attenuate = att.GetAtten (); // Read back if limited (setting.Attenuate was outside range) + old_settingAttenuate = setting.Attenuate; + } + + // pre-calculate adjustment for RSSI values + dBadjust = (double)setting.Attenuate - 120.0 + setting.LevelOffset - setting.ExternalGain + bpfCalibrations[bpfIndex]; + Serial.printf("SweepLo dBadjust = %f; leveloffset = %f; attenuate = %i, ext gain = %f, bpfCal = %f\n", + dBadjust, setting.LevelOffset, setting.Attenuate, setting.ExternalGain, bpfCalibrations[bpfIndex]); + + resetAverage = changedSetting; + + xmit.SetPowerReference ( setting.ReferenceOut ); // Set the GPIO reference output if wanted + + maxGrid = setting.MaxGrid; + minGrid = setting.MinGrid; + +#if USE_WIFI + // Vary number of points to send in each chunk depending on delaytime + // A chunk is sent at the end of each sweep regardless + wiFiPoints = wiFiTargetTime / delaytime; + if (wiFiPoints > MAX_WIFI_POINTS) + wiFiPoints = MAX_WIFI_POINTS; + if (wiFiPoints > displayPoints*OVERLAP) + wiFiPoints = displayPoints*OVERLAP; + //Serial.printf("No of wifiPoints set to %i\n", wiFiPoints); + + if ( numberOfWebsocketClients > 0 ) + pushSettings (); +#endif // #if USE_WIFI + +#if defined( TG_IF_INSTALLED ) && !defined( TG_LO_INSTALLED ) + if (tgIF_OK && (trackGenSetting.Mode == 1) ) + { + tg_if.TxMode ( trackGenSetting.IF_Drive ); // Set tracking generator IF on + Serial.println("tgif turned on"); + delayMicroseconds(300); + } + else + tg_if.RxMode(); +#endif + + +#if defined(TG_IF_INSTALLED) && defined(TG_LO_INSTALLED) + if (tgLO_OK && tgIF_OK) + { + switch ( trackGenSetting.Mode ) + { + case 0: // off + tg_if.RxMode(); + tg_lo.RxMode(); + break; + + case 1: // tracking + tg_if.TxMode ( trackGenSetting.IF_Drive ); // Set tracking generator IF on + tg_lo.TxMode ( trackGenSetting.LO_Drive ); // Set tracking generator LO on + break; + + case 2: // generator mode + tg_lo.TxMode ( trackGenSetting.LO_Drive ); // Set tracking generator LO on + tg_if.TxMode ( trackGenSetting.IF_Drive ); // Set tracking generator IF on + delayMicroseconds(300); + tg_lo.SetFrequency ( setting.IF_Freq + trackGenSetting.Offset + trackGenSetting.Frequency ); + break; + + default: + Serial.println("Invalid track gen mode in Sweeplo"); + } + } + +#endif + + } // initSweep || changedSetting + + autoSweepStep = 0; // Set the step counter to zero + sweepStep = 0; + autoSweepFreq = setting.ScanStart; // Set the start frequency. + + nextPointFreq = autoSweepFreq + autoSweepFreqStep; + + + /* Spur reduction offsets the IF from its normal value. LO also has to be offset same amount + * Offset should be more than half the RX bandwidth to ensure spur is still not in the RX filter passband + * but not so big that the frequencies fall outside the SAW filter passband. + * Use the average trace set to minimum to see the result. Spurs if any will be visible + * at different frequencies. + * Real signals will be present at the same frequency, so a min trace will show only real signals + * How well this works depends on how flat the SAW filter (and SI4432 filter) response is + */ + if (setting.Spur && spurToggle) { + uint32_t IF_Shift = requiredRBW10 * 100; // bandwidth in Hz + if (IF_Shift > MAX_IF_SHIFT) + IF_Shift = MAX_IF_SHIFT; + tempIF = offsetIF - IF_Shift; + } else { + tempIF = offsetIF; + } + + // track gen IF follows LO if only one SI4432, otherwise its offset by an amount to reduce blow by + if (tgLO_OK) + tgIF = tempIF + trackGenSetting.Offset; + else + tgIF = tempIF; + + + + spurToggle = !spurToggle; + //Serial.printf("tempIF %u, spurOffset=%i, spur:%i, Toggle:%i\n", tempIF, tempIF - setting.IF_Freq, setting.Spur, spurToggle); + + while (( micros() - setFreqMicros ) < delaytime ) // Make sure enough time has elasped since previous frequency write + { + } + + if ( ( lastIF != tempIF ) || initSweep || changedSetting ) + { + //Serial.printf("set rcvr Freq get:%u, tempIF:%u\n", rcvr.GetFrequency(), tempIF); + rcvr.SetFrequency ( tempIF ); // Set the RX Si4432 to the IF frequency + lastIF = tempIF; + actualIF = rcvr.GetFrequency(); + +#ifdef TG_IF_INSTALLED + if (tgIF_OK && (trackGenSetting.Mode == 1) ) + { + tg_if.SetFrequency ( tgIF ); // Set tracking generator IF for the sweep + //Serial.printf("tgif set to %i Hz\n", tgIF); + } +#endif + } + + xmit.SetFrequency ( tempIF + autoSweepFreq ); // set the LO frequency, tempIF is offset if spur reduction on + +#ifdef TG_LO_INSTALLED + if (tgLO_OK && (trackGenSetting.Mode == 1) ) + { + tg_lo.SetFrequency ( tgIF + autoSweepFreq ); // Set tracking generator LO + //Serial.printf("tglo set to %i Hz at start of sweep\n", tgIF + autoSweepFreq); + } +#endif + + setFreqMicros = micros(); // Store the time the frequency was changed + + + /* + * Actual frequency in the SI4432 is rounded and is limited by the possible resolution + */ + actualFreq = xmit.GetFrequency() - actualIF + RX_PASSBAND_OFFSET; // Used for next RSSI command and JSON entry + + +#if USE_WIFI + + if ( numberOfWebsocketClients > 0 ) // Start off the json document for the scan + { + jsonDocument.clear (); + chunkIndex = 0; + initChunkSweepDoc (sweepStep); + Points = jsonDocument.createNestedArray ( "Points" ); // Add Points array + jsonDocInitialised = true; + } + else + jsonDocInitialised = false; + +#endif // #if USE_WIFI + + startFreq = setting.ScanStart + tempIF; // Start freq for the LO + stopFreq = setting.ScanStop + tempIF; // Stop freq for the LO + + pointMinGain = 100; // Reset min/max values + pointMaxRSSI = 0; + + +/* +* Copy the values for the peaks (marker positions) to the old versions. No need to +* reset the indicies or frequencies; just the "Level". +*/ + for ( int i = 0; i < MARKER_COUNT; i++ ) + { + oldPeaks[i].Level = peaks[i].Level; + oldPeaks[i].Index = peaks[i].Index; + oldPeaks[i].Freq = peaks[i].Freq; + peaks[i].Level = 0; + } + + DisplayInfo (); // Display axis, top and side bar text + + peakLevel = 0; // Reset the peak values for the sweep + peakFreq = 0.0; + peakGain = 100; // Set to higher than gain can ever be + + lastMinRSSI = minRSSI; + minRSSI = 300; // Higher than it can be + + + + pointsPastPeak = 0; // Avoid possible peak detection at start of sweep + peakRSSI = 0; + maxRSSI = 0; + + sweepStartDone = true; // Make sure this initialize is only done once per sweep + initSweep = false; + changedSetting = false; + + if ( setActualPowerRequested ) + { + SetPowerLevel ( actualPower ); + setActualPowerRequested = false; +// Serial.printf ( "Setting actual Power %f \n", actualPower ); + } + + lastSweepStartMicros = sweepStartMicros; // Set last time we got here + sweepStartMicros = micros(); // Current time + sweepMicros = sweepStartMicros - lastSweepStartMicros; // Calculate sweep time (no rollover handling) + + + + } // End of "if ( !sweepStartDone ) || initSweep || changedSetting )" + + +/* +* Here we do the actual sweep. Save the current step and frequencies for the next time +* through, then wait the required amount of time based on the RBW before taking the +* signal strength reading and changing the transmitter (LO) frequency. +*/ + + uint16_t oldSweepStep = autoSweepStep; + uint32_t oldSweepFreq = actualFreq; + + /* + * Wait until time to take the next reading. If a long enough wait left + * then check the touchscreen and Websockets while we are waiting + * to improve response + */ + + + nowMicros = micros(); + + while (( nowMicros - setFreqMicros ) < delaytime ) + { + + if ( ( nowMicros - setFreqMicros + delaytime > MIN_DELAY_WEBSOCKETS ) && + ( (nowMicros - lastWebsocketMicros > websocketInterval) || (numberOfWebsocketClients > 0) ) ) + { +// Serial.print("W"); + // webSocket.loop (); // Check websockets - includes Yield() to allow other events to run +// Serial.println("w"); + lastWebsocketMicros = nowMicros; + } + if ( nowMicros - setFreqMicros > 100 ) // Wait some time to allow DMA sprite write to finish! + UiProcessTouch (); // Check the touch screen +// Serial.println("w"); + nowMicros = micros(); + } + + int rxRSSI = rcvr.GetRSSI (); // Read the RSSI from the RX SI4432 + +/* + * Note that there are two different versions of the print statement to send the + * RSSI readings to the serial output. You can change which one is commented out. + * + * The first one produces a tab separated list of just the frequency and RSSI + * reading. That format can be easily read inte something like Excel. + * + * The second one produces a listing more fit for human consumption! + */ + + if ( showRSSI ) // Displaying RSSI? + { +// Serial.printf ( "%s\t%03d\n", +// FormatFrequency ( autoSweepFreq) , rxRSSI ); // Send it to the serial output + Serial.printf ( "Actual IF: %s", FormatFrequency ( rcvr.GetFrequency() ) ); + Serial.printf ( " LO Freq: %s", FormatFrequency ( xmit.GetFrequency() ) ); + Serial.printf ( " Sweep Freq: %s", FormatFrequency ( autoSweepFreq) ); + Serial.printf ( " Actual Freq %s - RSSI: %03d\n", + FormatFrequency ( actualFreq ), rxRSSI ); // Send it to the serial output + } + + if ( (numberOfWebsocketClients > 0) || (setting.ShowGain) ) + gainReading = GetPreampGain ( &AGC_On, &AGC_Reg ); // Record the preamp/lna gains + + autoSweepFreq += sweepFreqStep; // Increment the frequency + sweepStep++; // and increment the step count + + +/* + * Change the transmitter frequency for the next reading and record the time for + * the RBW required settling delay. + */ + uint32_t f = tempIF + autoSweepFreq; + + xmit.SetFrequency ( f ); // Set the new LO frequency as soon as RSSI read +// Serial.printf("LO Required: %i Actual %i\n", tempIF+autoSweepFreq, xmit.GetFrequency()); + +#ifdef TG_LO_INSTALLED + if (tgLO_OK && (trackGenSetting.Mode == 1) ) + { + tg_lo.SetFrequency ( f + trackGenSetting.Offset ); // Set tracking generator LO + + } +#endif + + setFreqMicros = micros(); // Store the time the LO frequency was changed + +#ifdef TG_LO_INSTALLED +// if (trackGenSetting.Mode == 1) +// Serial.printf("tglo %i f=%i, lo=%02X, if=%02X\n", tg_lo.ReadFrequency()- tg_if.ReadFrequency(), autoSweepFreq, tg_lo.ReadByte(REG_OFC1) & 0x0F, tg_if.ReadByte(REG_OFC1) & 0x0F ); +#endif + +#if USE_WIFI + + if ( numberOfWebsocketClients > 0 ) + { + if ( jsonDocInitialised ) + { + JsonObject dataPoint = Points.createNestedObject (); // Add an object to the Json array to be pushed to the client + dataPoint["x"] = oldSweepFreq/1000000.0; // Set the x(frequency) value + dataPoint["y"] = rxRSSI; // Set the y (RSSI) value + dataPoint["g"] = gainReading; // Set the y (gain) value + // Serial.printf ( "Add point chunkIndex %u, sweepStep %u of %u \n", chunkIndex, sweepStep, sweepPoints); + chunkIndex++; // increment no of data points in current WiFi chunk + + if ( chunkIndex >= wiFiPoints ) // Send the chunk of data and start new jSon document + { + String wsBuffer; + + if ( wsBuffer ) + { + // Serial.print("D"); + serializeJson ( jsonDocument, wsBuffer ); + // Serial.printf("J%u", wsBuffer.length() ); + // Serial.println(wsBuffer); + unsigned long s = millis(); + webSocket.broadcastTXT ( wsBuffer ); // Send to all connected websocket clients + if (millis() - s > 1000) + { + Serial.println("webSocketTimeout"); + Serial.println(wsBuffer); + websocketFailCount++; + if (websocketFailCount > 2) + numberOfWebsocketClients = 0; + } else { + websocketFailCount = 0; // reset if OK + } + // Serial.print("j"); + } + + else + Serial.println("No buffer :("); + } + } + if ( ( chunkIndex >= wiFiPoints ) || !jsonDocInitialised ) // Start new jSon document + { + chunkIndex = 0; + initChunkSweepDoc (sweepStep); + Points = jsonDocument.createNestedArray ( "Points" ); // Add Points array + jsonDocInitialised = true; + } + } + +/* + * Actual frequency in the SI4432 is rounded and is limited by the possible resolution + */ + actualFreq = xmit.GetFrequency() - actualIF + RX_PASSBAND_OFFSET; // Used for next RSSI command and JSON entry + +#endif // #if USE_WIFI + + if ( rxRSSI > pointMaxRSSI ) // RSSI > maximum value for this point so far? + { + myActual[autoSweepStep] = rxRSSI; // Yes, save it + pointMaxRSSI = rxRSSI; // Added by G3ZQC - Remember new maximim + pointMaxFreq = oldSweepFreq; + } + + if ( gainReading < pointMinGain ) // Gain < minimum gain for this point so far? + { + myGain[autoSweepStep] = gainReading; // Yes, save it + pointMinGain = gainReading; // Added by G3ZQC - Remember new minimum + } + + if (rxRSSI < minRSSI) // Detect minimum for sweep + minRSSI = rxRSSI; + + +/* + * Have we enough readings for this display point? If yes, so do any averaging etc, reset + * the values so peak in the frequency step is recorded and update the display. + */ + + if ( autoSweepFreq >= nextPointFreq ) + { + nextPointFreq = nextPointFreq + autoSweepFreqStep; // Next display point frequency + autoSweepStep++; // Increment the index + + pointMinGain = 100; // Reset min/max values + pointMaxRSSI = 0; + + DrawCheckerBoard ( oldSweepStep ); // Draw the grid for the point in the sweep we have just read + + if ( resetAverage || setting.Average == AV_OFF ) // Store data, either as read or as rolling average + myData[oldSweepStep] = myActual[oldSweepStep]; + + else + { + switch ( setting.Average ) + { + case AV_MIN: + if ( myData[oldSweepStep] > myActual[oldSweepStep] ) + myData[oldSweepStep] = myActual[oldSweepStep]; + break; + + case AV_MAX: + if ( myData[oldSweepStep] < myActual[oldSweepStep] ) + myData[oldSweepStep] = myActual[oldSweepStep]; + break; + + case AV_2: + myData[oldSweepStep] = ( myData[oldSweepStep] + myActual[oldSweepStep] ) / 2; + break; + + case AV_4: + myData[oldSweepStep] = ( myData[oldSweepStep]*3 + myActual[oldSweepStep] ) / 4; + break; + + case AV_8: + myData[oldSweepStep] = ( myData[oldSweepStep]*7 + myActual[oldSweepStep] ) / 8; + break; + } + DisplayPoint ( myData, oldSweepStep, AVG_COLOR ); + } + + if ( setting.ShowSweep ) + DisplayPoint ( myActual, oldSweepStep, DB_COLOR ); + + if ( setting.ShowGain ) + displayGainPoint ( myGain, oldSweepStep, GAIN_COLOR ); + + if ( setting.ShowStorage ) + DisplayPoint ( myStorage, oldSweepStep, STORAGE_COLOR ); + + if ( setting.SubtractStorage ) + rxRSSI = 128 + rxRSSI - myStorage[oldSweepStep]; + + +/* + * Record the peak values but not if freq low enough to detect the LO + */ + + if (( autoSweepFreq > MARKER_MIN_FREQUENCY ) && (oldSweepStep > 0)) + { + if ( maxRSSI <= myActual[oldSweepStep] ) + { + maxIndex = oldSweepStep; + maxRSSI = myActual[oldSweepStep]; + maxFreq = oldSweepFreq; + +// Serial.printf( "peakLevel set %i, index %i\n", peakLevel, oldSweepStep); +// displayPeakData (); + } + + +/* + * Save values used by peak detection. Need to save the previous value as we only + * know we have a peak once past it! + */ + + prevPointRSSI = currentPointRSSI; + currentPointRSSI = myData[oldSweepStep]; + + +/* + * Peak point detection. Four peaks, used to position the markers + */ + if ( currentPointRSSI >= prevPointRSSI ) // Level or ascending + { + pointsPastDip ++; + if ( pointsPastDip == PAST_PEAK_LIMIT ) + { + pointsPastPeak = 0; + } + + if ( currentPointRSSI > peakRSSI ) + { + peakRSSI = currentPointRSSI; // Store values + peakFreq = oldSweepFreq; + peakIndex = oldSweepStep; + } + } + + else + { + pointsPastPeak ++; // only a true peak if value decreased for a number of consecutive points + + if ( pointsPastPeak == PAST_PEAK_LIMIT ) // We have a peak + { + pointsPastDip = 0; + +/* + * Is this peak bigger than previous ones? Only check if bigger than smallest peak so far + */ + + if ( peakRSSI > peaks[MARKER_COUNT-1].Level ) + { + for ( uint16_t p = 0; p < MARKER_COUNT; p++ ) + { + if ( peakRSSI > peaks[p].Level ) + { + for ( uint16_t n = 3; n > p; n-- ) // Shuffle lower level peaks down + memcpy ( &peaks[n], &peaks[n-1], sizeof ( peak_t )); + + peaks[p].Level = peakRSSI; // Save the peak values + peaks[p].Freq = peakFreq; + peaks[p].Index = peakIndex; + break; + } + } + } + + peakRSSI = 0; // Reset peak values ready for next peak + } // We have a peak + } // Descending + } // if (( autoSweepFreq > 1000000 ) && (oldSweepStep > 0)) + + +/* + * Draw the markers if main sweep is displayed. The markers know if they are enabled or not + * Only paint if sweep step is in range where there will be a marker + */ + + if ( setting.ShowSweep || setting.Average != AV_OFF ) + { + for ( int p = 0; p < MARKER_COUNT; p++ ) + if (( abs ( oldSweepStep - oldPeaks[p].Index ) + <= MARKER_SPRITE_HEIGHT / 2 ) && ( oldPeaks[p].Level > (lastMinRSSI + MARKER_NOISE_LIMIT) )) + + marker[p].Paint ( &img, oldPeaks[p].Index - oldSweepStep, + rssiToImgY ( oldPeaks[p].Level ) ); + } + + // If in the last few points and gain trace is displayed show the gain scale + if ( setting.ShowGain && (oldSweepStep > displayPoints - 2 * CHAR_WIDTH) ) + { + int16_t scaleX = displayPoints - 2 * CHAR_WIDTH - oldSweepStep + 1; // relative to the img sprite + img.setPivot( scaleX, 0); + gainScaleSprite.pushRotated ( &img, 0, TFT_BLACK ); // Send the sprite to the target sprite, with transparent colour + } + + if ( oldSweepStep > 0 ) // Only push if not first point (two pixel wide img) + img.pushSprite ( xOrigin+oldSweepStep-1, yOrigin ); + +// myFreq[oldSweepStep] = oldSweepFreq; // Store the frequency for XML file creation + + } // End of "if ( autoSweepFreq >= nextPointFreq )" + + if ( sweepStep >= sweepPoints ) // If we have got to the end of the sweep + { +// autoSweepStep = 0; + sweepStartDone = false; + resetAverage = false; + + if ( sweepCount < 2 ) + sweepCount++; // Used to disable wifi at start + +// Serial.printf("MaxRSSI = %i, freq = %i\n", maxRSSI, maxFreq); + + oldPeakLevel = maxRSSI; //Save value of peak level for use by the "SetPowerLevel" function + + if ( myActual[displayPoints-1] == 0 ) // Ensure a value in last data point + { + myActual[displayPoints-1] = rxRSSI; // Yes, save it + myGain[displayPoints-1] = gainReading; +// myFreq[displayPoints-1] = oldSweepFreq; + } + + if ( showRSSI == 1 ) // Only show it once? + showRSSI = 0; // Then turn it off + +#if USE_WIFI + + if (( numberOfWebsocketClients > 0) && jsonDocInitialised && (chunkIndex > 0) ) + { + + String wsBuffer; + + if (wsBuffer) + { + serializeJson ( jsonDocument, wsBuffer ); + webSocket.broadcastTXT ( wsBuffer ); // Send to all connected websocket clients + } + + else + Serial.println ( "No buffer :("); + } + +#endif // #if USE_WIFI + + } // End of "if ( sweepStep >= sweepPoints )" +} // End of "doSweepLow" diff --git a/TFT_eSPI Setup Files/User_Setup_Select.h b/TFT_eSPI Setup Files/User_Setup_Select.h new file mode 100644 index 0000000..32e114e --- /dev/null +++ b/TFT_eSPI Setup Files/User_Setup_Select.h @@ -0,0 +1,139 @@ +// This header file contains a list of user setup files and defines which one the +// compiler uses when the IDE performs a Verify/Compile or Upload. +// +// Users can create configurations for different Espressif boards and TFT displays. +// This makes selecting between hardware setups easy by "uncommenting" one line. + +// The advantage of this hardware configuration method is that the examples provided +// with the library should work with different setups immediately without any other +// changes being needed. It also improves the portability of users sketches to other +// hardware configurations and compatible libraries. +// +// Create a shortcut to this file on your desktop to permit quick access for editing. +// Re-compile and upload after making and saving any changes to this file. + +// Customised User_Setup files are stored in the "User_Setups" folder. + +#ifndef USER_SETUP_LOADED // Lets PlatformIO users define settings in + // platformio.ini, see notes in "Tools" folder. + +// Only ONE line below should be uncommented. Add extra lines and files as needed. + +//#include // Default setup is root library folder + +//#include // Setup file configured for ILI9341 on TinySA +#include // Setup file configured for ILI9341 on TinySA +//#include // Setup file configured for my ILI9341 +//#include // Setup file configured for my ST7735 +//#include // Setup file configured for my ILI9163 +//#include // Setup file configured for my S6D02A1 +//#include // Setup file configured for my stock RPi TFT +//#include // Setup file configured for my modified RPi TFT +//#include // Setup file configured for my ST7735 128x128 display +//#include // Setup file configured for my ILI9163 128x128 display +//#include // Setup file configured for my ST7735 +//#include // Setup file configured for ESP8266 and RPi TFT with touch + +//#include // Setup file configured for ESP32 and RPi TFT with touch +//#include // Setup file for the ESP32 based M5Stack +//#include // Setup file for the ESP32 with parallel bus TFT +//#include // Setup file for the ESP32 with parallel bus TFT +//#include // Setup file configured for HX8357D (untested) +//#include // Setup file for the ESP32 with parallel bus TFT +//#include // Setup file for any Waveshare ePaper display +//#include // Setup file configured for HX8357D (untested) + +//#include // Setup file for ESP8266 and ILI9488 SPI bus TFT +//#include // Setup file for ESP32 and ILI9488 SPI bus TFT + +//#include // Setup file for ESP32 and TTGO T4 (BTC) ILI9341 SPI bus TFT +//#include // Setup file for ESP32 and TTGO TM ST7789 SPI bus TFT + +//#include // Setup file configured for my ST7735S 80x160 + +//#include + + +#endif // USER_SETUP_LOADED + + + +///////////////////////////////////////////////////////////////////////////////////// +// // +// DON'T TINKER WITH ANY OF THE FOLLOWING LINES, THESE ADD THE TFT DRIVERS // +// AND ESP8266 PIN DEFINITONS THEY ARE HERE FOR BODMER'S CONVENIENCE! // +// // +///////////////////////////////////////////////////////////////////////////////////// + + +// Identical looking TFT displays may have a different colour ordering in the 16 bit colour +#define TFT_BGR 0 // Colour order Blue-Green-Red +#define TFT_RGB 1 // Colour order Red-Green-Blue + + +// Load the right driver definition - do not tinker here ! +#if defined (ILI9341_DRIVER) + #include + #define TFT_DRIVER 0x9341 +#elif defined (ST7735_DRIVER) + #include + #define TFT_DRIVER 0x7735 +#elif defined (ILI9163_DRIVER) + #include + #define TFT_DRIVER 0x9163 +#elif defined (S6D02A1_DRIVER) + #include + #define TFT_DRIVER 0x6D02 +#elif defined (RPI_ILI9486_DRIVER) + #include + #define TFT_DRIVER 0x9486 +#elif defined (ILI9486_DRIVER) + #include + #define TFT_DRIVER 0x9486 +#elif defined (ILI9481_DRIVER) + #include + #define TFT_DRIVER 0x9481 +#elif defined (ILI9488_DRIVER) + #include + #define TFT_DRIVER 0x9488 +#elif defined (HX8357D_DRIVER) + #include "TFT_Drivers/HX8357D_Defines.h" + #define TFT_DRIVER 0x8357 +#elif defined (EPD_DRIVER) + #include "TFT_Drivers/EPD_Defines.h" + #define TFT_DRIVER 0xE9D +#elif defined (ST7789_DRIVER) + #include "TFT_Drivers/ST7789_Defines.h" + #define TFT_DRIVER 0x7789 +#elif defined (R61581_DRIVER) + #include "TFT_Drivers/R61581_Defines.h" + #define TFT_DRIVER 0x6158 +#elif defined (XYZZY_DRIVER) // <<<<<<<<<<<<<<<<<<<<<<<< ADD NEW DRIVER HERE + #include "TFT_Drivers/XYZZY_Defines.h" + #define TFT_DRIVER 0x0000 +#else + #define TFT_DRIVER 0x0000 +#endif + + +// These are the pins for all ESP8266 boards +// Name GPIO Function +#define PIN_D0 16 // WAKE +#define PIN_D1 5 // User purpose +#define PIN_D2 4 // User purpose +#define PIN_D3 0 // Low on boot means enter FLASH mode +#define PIN_D4 2 // TXD1 (must be high on boot to go to UART0 FLASH mode) +#define PIN_D5 14 // HSCLK +#define PIN_D6 12 // HMISO +#define PIN_D7 13 // HMOSI RXD2 +#define PIN_D8 15 // HCS TXD0 (must be low on boot to enter UART0 FLASH mode) +#define PIN_D9 44 // RXD0 +#define PIN_D10 43 // TXD0 + +#define PIN_MOSI 8 // SD1 +#define PIN_MISO 7 // SD0 +#define PIN_SCLK 6 // CLK +#define PIN_HWCS 0 // D3 + +#define PIN_D11 9 // SD2 +#define PIN_D12 10 // SD4 diff --git a/TFT_eSPI Setup Files/WA2FZW_Setup_ILI9341_TinySA.h b/TFT_eSPI Setup Files/WA2FZW_Setup_ILI9341_TinySA.h new file mode 100644 index 0000000..d5f1acb --- /dev/null +++ b/TFT_eSPI Setup Files/WA2FZW_Setup_ILI9341_TinySA.h @@ -0,0 +1,37 @@ +// See SetupX_Template.h for all options available + +#define ILI9341_DRIVER + +#define USE_HSPI_PORT // default is VSPI +#define TFT_DC 27 +#define TFT_CS 15 +#define TFT_MOSI 13 +#define TFT_SCLK 14 +#define TFT_MISO 12 +#define TFT_LED 25 +#define TOUCH_CS 26 +#define TFT_RST 2 +//#define TFT_RST -1 // Set TFT_RST to -1 if the display RESET is connected to NodeMCU RST or 3.3V + + +#define LOAD_GLCD // Font 1. Original Adafruit 8 pixel font needs ~1820 bytes in FLASH +#define LOAD_FONT2 // Font 2. Small 16 pixel high font, needs ~3534 bytes in FLASH, 96 characters +#define LOAD_FONT4 // Font 4. Medium 26 pixel high font, needs ~5848 bytes in FLASH, 96 characters +#define LOAD_FONT6 // Font 6. Large 48 pixel font, needs ~2666 bytes in FLASH, only characters 1234567890:-.apm +#define LOAD_FONT7 // Font 7. 7 segment 48 pixel font, needs ~2438 bytes in FLASH, only characters 1234567890:. +#define LOAD_FONT8 // Font 8. Large 75 pixel font needs ~3256 bytes in FLASH, only characters 1234567890:-. +#define LOAD_GFXFF // FreeFonts. Include access to the 48 Adafruit_GFX free fonts FF1 to FF48 and custom fonts + + +#define SMOOTH_FONT + + #define SPI_FREQUENCY 27000000 +// #define SPI_FREQUENCY 40000000 +// #define SPI_FREQUENCY 80000000 + +#define SPI_READ_FREQUENCY 20000000 + +#define SPI_TOUCH_FREQUENCY 2500000 + + +// #define SUPPORT_TRANSACTIONS diff --git a/TFT_eSPI Setup Files/simpleSA_Setup_ILI9341.h b/TFT_eSPI Setup Files/simpleSA_Setup_ILI9341.h new file mode 100644 index 0000000..7e8b2d4 --- /dev/null +++ b/TFT_eSPI Setup Files/simpleSA_Setup_ILI9341.h @@ -0,0 +1,37 @@ +// See SetupX_Template.h for all options available + +#define ILI9341_DRIVER + +#define USE_HSPI_PORT // default is VSPI +#define TFT_DC 27 +#define TFT_CS 15 +#define TFT_MOSI 13 +#define TFT_SCLK 14 +#define TFT_MISO 12 +//#define TFT_LED 25 +#define TOUCH_CS 26 +//#define TFT_RST 2 +#define TFT_RST -1 // Set TFT_RST to -1 if the display RESET is connected to ESP32 RST or 3.3V + + +#define LOAD_GLCD // Font 1. Original Adafruit 8 pixel font needs ~1820 bytes in FLASH +#define LOAD_FONT2 // Font 2. Small 16 pixel high font, needs ~3534 bytes in FLASH, 96 characters +#define LOAD_FONT4 // Font 4. Medium 26 pixel high font, needs ~5848 bytes in FLASH, 96 characters +#define LOAD_FONT6 // Font 6. Large 48 pixel font, needs ~2666 bytes in FLASH, only characters 1234567890:-.apm +#define LOAD_FONT7 // Font 7. 7 segment 48 pixel font, needs ~2438 bytes in FLASH, only characters 1234567890:. +#define LOAD_FONT8 // Font 8. Large 75 pixel font needs ~3256 bytes in FLASH, only characters 1234567890:-. +#define LOAD_GFXFF // FreeFonts. Include access to the 48 Adafruit_GFX free fonts FF1 to FF48 and custom fonts + + +#define SMOOTH_FONT + +//#define SPI_FREQUENCY 27000000 +#define SPI_FREQUENCY 40000000 +// #define SPI_FREQUENCY 80000000 + +#define SPI_READ_FREQUENCY 20000000 + +#define SPI_TOUCH_FREQUENCY 2500000 + + +// #define SUPPORT_TRANSACTIONS diff --git a/cmd.cpp b/cmd.cpp new file mode 100644 index 0000000..06a24b7 --- /dev/null +++ b/cmd.cpp @@ -0,0 +1,3520 @@ +/* + * "Cmd.cpp" was added in Version 2.0 by John Price (WA2FZW) + * + * This file contains the functions to read a command and the associated data + * from the serial input and all the functions necessary to process the + * commands. + * + * The functions to read and interpret the commands were stripped out of the + * "loop" function in the main program file. As it currently exists, the command + * structure has been modified to be more human friendly, and will no longer work + * with Erik's "TintSA.exe" program, which has been replaced with a web page + * interface which is much more flexibile. + * + * The other functions in here all support the main command interpreter and are + * also used by the "ui.cpp" and "TinySA_wifi.cpp" modules. Eventually, we hope + * that those modules will also be able to use the "ProcessCommand" function in + * here in place of much redundant code in those modules. + */ + +#include "cmd.h" // Our associated header +#include "preferences.h" // For "SAVE" & "RECALL" +#include "marker.h" // Marker class definition +/* + * These are the objects for the PE4302 and Si4432 modules. They are created in the + * main program file: + */ + +extern PE4302 att; // PE4302 Attenuator object +extern Si4432 rcvr; // Si4432 Receiver object +extern Si4432 xmit; // Si4432 Transmitter object +#ifdef TG_IF_INSTALLED +extern Si4432 tg_if; // Si4432 Transmitter object +#endif +#ifdef TG_LO_INSTALLED +extern Si4432 tg_lo; // Si4432 Transmitter object +#endif + +/* + * Global variables defined in the main program file: + */ + +extern settings_t setting; // Structure to track & save settings +extern bandpassFilter_t bandpassFilters[]; // RBW Setting data + +extern uint16_t bpfCount; // Number of entries in "bandpassFilters" +extern int changedSetting; // Something in "setting" changed +extern uint16_t bpfCalibrate; // set true if a SI4432 bandpass filter calibration run is taking place + +extern uint16_t tinySA_mode; + +/* + * Variables to determine size of grid and waterfall + * In Bandscope mode the grid is reduced. In future it may be possible to add + * a waterfall to the main sweep, but not yet + */ +extern uint16_t gridHeight; +extern uint16_t gridWidth; +extern uint16_t yGrid; // no of grid divisions +extern uint16_t yDelta; // no of points/division +extern uint16_t xGrid; +extern uint16_t xOrigin; +extern uint16_t yOrigin; +extern uint16_t displayPoints; +extern uint16_t xDelta; +extern uint16_t waterfallHeight; + + +extern uint8_t myData[SCREEN_WIDTH+1]; +extern uint8_t myStorage[SCREEN_WIDTH+1]; + +extern float bandwidth; // The current bandwidth (not * 10) +extern unsigned long delaytime; // In microseconds +extern uint16_t bpfIndex; // RBW bandpass Filter in use +extern unsigned long offsetDelayTime; // In microseconds + +extern unsigned long wiFiTargetTime; +extern uint16_t wiFiPoints; +extern unsigned long websocketInterval; // time in microseconds between websocket polls if no wifi client + +extern int VFO; // TX/RX Si4432 selector + +extern uint16_t steps; // Number of frequency steps in the sweep (not really used) + +extern bool setActualPowerRequested; +extern float actualPower; + +extern uint16_t oldPeakLevel; // Current maximum signal level (was peakLevel) G3ZQC + +int32_t frequency0 = 0; // Used in setting sweep configuration +int32_t frequency1 = 100000000; + +extern uint32_t lastStart; // Last value of the sweep start frequency +extern uint32_t lastStop; // Last value of the sweep end frequency + +extern uint32_t startFreq_IF; // Last value of the IF sweep start frequency +extern uint32_t stopFreq_IF; // Last value of the IF sweep end frequency +extern uint32_t sigFreq_IF; // Last value of the IF sweep end frequency + +extern uint32_t startFreq_RX; // Last value of the RX sweep start frequency +extern uint32_t stopFreq_RX; // Last value of the RX sweep end frequency +extern uint32_t sigFreq_RX; // Last value of the RX sweep end frequency + +extern uint8_t showRSSI; // When true, RSSI readings are displayed +extern int initSweep; // Set by the "RSSI" command to restart the sweep +extern bool paused; + +extern uint32_t colourTest; + +extern Marker marker[MARKER_COUNT]; // Array of marker objects + +extern uint16_t sigGenOutputOn; // signal generator state - 0 = off, 1 = on + +/* + * Functions still in the main program file. Maybe some need to move here? + */ + +extern void DisplayInfo (); +extern void DisplayError ( uint8_t severity, const char *line1, const char *line2, const char *line3, const char *line4 ); +extern void RedrawHisto (); +extern void pushSettings (); +extern void pushBandscopeSettings (); +extern void initSweepLow (); +extern void initSigLow (); +extern void initIF_Sweep (); +extern void setMode (uint16_t newMode); +extern uint8_t dBmToRSSI ( double dBm ); + + +/* + * The "msgTable" provides a way of translating the ASCII message to the internal + * number. Needs to be re-ordered based on expected usage. + */ + + msg_t msgTable[] = + { + { "START", MSG_START }, // Set sweep start frequency + { "STOP", MSG_STOP }, // Set sweep stop frequency + { "CENTER", MSG_CENTER }, // Set sweep center frequency + { "SPAN", MSG_SPAN }, // Set sweep range + { "FOCUS", MSG_FOCUS }, // Set sweep center frequency with narrow bandwidth + { "PREAMP", MSG_PREAMP }, // Set the receiver preamp gain + { "GAIN", MSG_PREAMP }, // Alternate receiver preamp gain + { "DRIVE", MSG_DRIVE }, // Set transmitter (LO) output level + { "FREQ", MSG_VFO_FREQ }, // Set the frequency for the selected VFO + { "ATTEN", MSG_ATTEN }, // Set the PE4301 attenuation + { "HELP", MSG_HELP }, // Display the command menu + { "?", MSG_HELP }, // Display the command menu + { "STEPS", MSG_STEPS }, // Set or get number of sweep points (not used) + { "DELAY", MSG_DELAY }, // Set or get delay time between sweep readings + { "VFO", MSG_VFO }, // Set or get the currently selected VFO + { "RBW", MSG_RBW }, // Set or get the current resolution bandwidth + { "REGDUMP", MSG_REG_DUMP }, // Print register values for the selected VFO + { "RSSI", MSG_RSSI }, // Show RSSI values + { "QUIT", MSG_QUIT }, // Stop RSSI readings + { "REGISTER", MSG_SET_REG }, // Set or get the value of a specific register for the selected VFO + { "SAVE", MSG_SAVE }, // Save scan configuration + { "RECALL", MSG_RECALL }, // Recall a saved scan configuration + { "REF_FREQ", MSG_GPIO2 }, // Set transmitter GPIO2 frequency + { "TUNE", MSG_TUNE }, // Tune the selected Si4431 (VFO) + { "CONFIG_SAVE", MSG_CONFIG }, // Save "config" structure + { "ACTUAL_PWR", MSG_ACT_PWR }, // Calibrate the indicated power level + { "IF_FREQ", MSG_IF_FREQ }, // Change the IF (receiver) frequency + { "TRACES", MSG_TRACES }, // Turn traces on the display on or off + { "GRIDREF", MSG_GRID }, // Set dB value for the top line of the grid + { "SCALE", MSG_SCALE }, // Set dB/horizontal line value for the grid + { "PAUSE", MSG_PAUSE }, // Pause at end of sweep - toggled + { "SALO", MSG_SWEEPLO }, // Sweep low range mode + { "SGLO", MSG_SIGLO }, // Signal generator low range mode + { "IFSWEEP", MSG_IF_SWEEP }, // IF sweep mode + { "BANDSCOPE", MSG_BANDSCOPE }, // BANDSCOPE mode + { "MARKER", MSG_MARKER }, // Set marker status and color + { "SPUR", MSG_SPUR }, // Set Spur reduction on/off + { "WIFITIME", MSG_WIFI_UPDATE }, // Set WiFi Update target time + { "WIFIPTS", MSG_WIFI_POINTS }, // Set WiFi points + { "SKTINT", MSG_WEBSKT_INTERVAL }, // Set WebSocket interval + { "SGRXDRIVE", MSG_SG_RX_DRIVE }, // Set Signal Generator RX Drive level + { "SGLODRIVE", MSG_SG_LO_DRIVE }, // Set Signal Generator LO Drive level + { "TGIFDRIVE", MSG_TG_IF_DRIVE }, // Set Track Generator IF Drive level + { "TGLODRIVE", MSG_TG_LO_DRIVE }, // Set Track Generator LO Drive level + { "IFSIGNAL", MSG_IFSIGNAL }, // IF sweep signal frequency + { "TGOFFSET", MSG_TGOFFSET }, // Offset TG IF from SA IF to reduce pass through + { "CTEST", MSG_COLOURTEST}, // Work out how colours work! + { "OFFDEL", MSG_OFFDELAY }, // Offset tuning delay + { "WFMIN", MSG_WFMIN }, // Min level for waterfall colours + { "WFGAIN", MSG_WFGAIN }, // Gain for waterfall colours + { "RXSWEEP", MSG_RX_SWEEP }, // Set RX Sweep Mode + { "RXSIGNAL", MSG_RXSIGNAL }, // IF sweep signal frequency + { "EXTGAIN", MSG_EXTGAIN }, // External gain or attenuation + { "SGON", MSG_SGON }, // turn on signal generator output + { "SGOFF", MSG_SGOFF }, // turn off signal generator output + { "SGFREQ", MSG_SGFREQ }, // Set Signal Generator Frequency + { "TRACKON", MSG_TGON }, // turn on tracking generator output + { "TRACKOFF", MSG_TGOFF }, // turn off tracking generator output + { "TRACKSIG", MSG_TGSIG }, // turn off tracking generator output + { "TGFREQ", MSG_TGFREQ }, // Set Track gen freq for sig gen mode + { "BPFCAL", MSG_BPFCAL }, // Start bandpass filter calibration + { "OTA", MSG_OTA }, // Select OTA Update Mode + + { "", MSG_NONE } // Unrecognized command + }; + +int msgCount = ELEMENTS ( msgTable); + + +/* + * "ShowMenu" displays the command menu on the USB output (Arduino IDE's Serial + * Monitor, or some other terminal emulator program). + */ + +void ShowMenu () +{ + Serial.printf ( "\nsimpleSA Spectrum Analyzer %s - User Commands:\n\n", PROGRAM_VERSION ); + Serial.println ( "Sweep Settings:\n" ); + + Serial.print ( " START.........Sweep start frequency, currently: " ); + Serial.println ( FormatFrequency ( setting.ScanStart )); + + Serial.print ( " STOP..........Sweep stop frequency, currently: " ); + Serial.println ( FormatFrequency ( setting.ScanStop )); + + Serial.print ( " CENTER........Sweep center frequency, currently: " ); + Serial.println ( FormatFrequency ( GetSweepCenter() )); + + Serial.print ( " SPAN..........Sweep frequency span, currently: " ); + Serial.println ( FormatFrequency ( setting.ScanStop - setting.ScanStart )); + + Serial.println ( " MARKER........It's complicated; read the documentation!" ); + + Serial.println ( " FOCUS.........Set single frequency mode with narrow bandwidth" ); + + Serial.printf ( " RBW...........Set or get resolution bandwidth (RBW); currently: %.1f KHz\n", bandwidth ); + + Serial.printf ( " ATTEN.........Set or get the attenuator setting; currently: %ddB\n", + setting.Attenuate ); + + Serial.printf ( " EXTGAIN.......Set or get the external gain setting; currently: %ddB\n", + setting.ExternalGain ); + + Serial.printf ( " SPUR......... Turn Spur Reduction 'ON' or 'OFF'; currently: %s\n", + setting.Spur ? "ON" : "OFF" ); + + Serial.println ( " PAUSE.........Pause or resume the sweep" ); + + + Serial.println ( "\nMode selection:\n" ); + + Serial.println ( " SALO..........Set to analyse mode low frequency range" ); + Serial.println ( " SGLO..........Set to signal generator mode low frequency range" ); + Serial.println ( " IFSWEEP.......Set to IF Sweep mode to analyse the TinySA SAW filters" ); + Serial.println ( " RXSWEEP.......Set to RX Sweep mode to analyse the TinySA FIR filters" ); + Serial.println ( " BANDSCOPE.....Set to BANDSCOPE mode" ); + Serial.println ( " OTA...........Set to OTA update mode to download firmware or SPIFFS over wifi" ); + + Serial.println ( "\nDisplay Options:\n" ); + + Serial.println ( " TRACES........Turn display traces on or off ['GAIN' or 'dB']" ); + + Serial.printf ( " PREAMP/GAIN...Set or get the receiver preamp gain\n" ); + Serial.println ( " See documentation for allowed values" ); + + Serial.printf ( " GRIDREF.......Set the grid reference level; currently: %ddB\n", setting.MaxGrid ); + + Serial.printf ( " SCALE.........Set the dB/horizontal line value; currently: %ddB\n", setting.PowerGrid ); + + Serial.printf ( " WFMIN.........Set the minimum RSSI level for waterfall colouring: %i\n", setting.WaterfallMin ); + Serial.printf ( " WFGAIN........Set the gain for waterfall colouring: %d\n", setting.WaterfallGain ); + + + Serial.println ( "\nSignal Generator Commands:\n" ); + + Serial.println ( " SGON..........Turn on signal generator output" ); + Serial.print ( " SGOFF.........Turn off signal generator output; currently " ); + Serial.println ( sigGenOutputOn ); + + Serial.print ( " SGFREQ........Signal Generator frequency, currently: " ); + Serial.println ( FormatFrequency ( sigGenSetting.Frequency )); + + Serial.print ( " SGLODRIVE.....Local oscillator drive level in signal generator mode [0 to 7]; currently: " ); + Serial.println ( sigGenSetting.LO_Drive ); + + Serial.print ( " SGRXDRIVE.....RX SI4432 oscillator drive level in signal generator mode [0 to "); + Serial.print ( RX_SI4432_MAX_DRIVE ); + Serial.print ( "]; currently: " ); + Serial.println ( sigGenSetting.RX_Drive ); + +#ifdef TG_IF_INSTALLED + Serial.println ( "\nTracking Generator Commands:\n" ); + + Serial.println ( " TRACKON.......Turn on tracking generator output" ); +#ifdef TG_LO_INSTALLED + Serial.println ( " TRACKSIG......Turn on tracking generator signal generator output" ); +#endif + Serial.print ( " TRACKOFF......Turn off tracking generator output; currently " ); + Serial.println ( trackGenSetting.Mode ); + +#ifdef TG_LO_INSTALLED + Serial.print ( " TGLODRIVE.....Local oscillator drive level in tracking generator mode [0 to 7]; currently: " ); + Serial.println ( trackGenSetting.LO_Drive ); +#endif + Serial.print ( " TGIFDRIVE.....Local oscillator drive level in tracking generator mode [0 to 7]; currently: " ); + Serial.println ( trackGenSetting.IF_Drive ); +#ifdef TG_LO_INSTALLED + Serial.print ( " TGOFFSET......Tracking generator offset from SA IF; currently: " ); + Serial.println ( trackGenSetting.Offset ); + Serial.print ( " TGFREQ........Tracking generator signal generator frequency; currently: " ); + Serial.println ( trackGenSetting.Frequency ); +#endif +#endif + + + Serial.println ( "\nOther Commands:\n" ); + + Serial.print ( " DRIVE.........Local oscillator drive level [0 to 7]; currently: " ); + Serial.println ( setting.Drive ); + + Serial.println ( " SAVE .........Save the current scan configuration [0 to 4]" ); + + Serial.println ( " RECALL........Recall a saved scan configuration [0 to 4]" ); + + Serial.println ( " FREQ..........Set or get the frequency for the selected VFO" ); + + Serial.println ( " HELP (or ?)...Show this menu" ); + + Serial.printf ( " STEPS.........Sweep samples; currently: %u\n", steps ); + + Serial.printf ( " DELAY.........Timestep in uS, currently: %uuS\n", delaytime ); + + Serial.printf ( " OFFDEL........Timestep in uS for Bandscope, currently: %uuS\n", offsetDelayTime ); + + Serial.print ( " VFO...........Set or get active VFO [R, ( L or T ), I(tg If), G(tG LO)]; currently: " ); + + if ( VFO == RX_4432 ) + Serial.println ( "RX (0)" ); + else if (VFO == TX_4432 ) + Serial.println ( "LO (1)" ); + else if (VFO == TGIF_4432 ) + Serial.println ( "TGIF (2)" ); + else if (VFO == TGLO_4432 ) + Serial.println ( "TGLO (3)" ); + + Serial.println ( "\nDebugging and Troubleshooting Commands:\n" ); + + Serial.printf ( " IF_FREQ.......Set the IF (receiver) frequency [433 to 435MHz]; currently: %s\n", + FormatFrequency ( setting.IF_Freq )); + + Serial.println ( " REGDUMP.......Display a dump of all the registers for the selected VFO" ); + + Serial.println ( " RSSI..........Output RSSI readings until the 'Q' command is entered" ); + Serial.println ( " or one time only ['C' or '1']; the default is once."); + + Serial.println ( " QUIT (or Q)...Terminate RSSI output" ); + + Serial.println ( " REGISTER......Read or write a Si4432 register for the selected VFO" ); + + Serial.println ( " REF_FREQ......Set or get the transmitter GPIO2 reference frequency" ); + + Serial.println ( " TUNE..........Tune the frequency of the selected VFO" ); + + Serial.println ( " ACTUAL_PWR....Calibrate the indicated power level" ); + + Serial.println ( " CONFIG_SAVE...Save the hardware configuration parameters" ); + + Serial.println ( " WiFiTIME......Target web page Chart Update time interval in ms" ); + + Serial.println ( " WiFiPTS.......No of points in each data chunk pushed to the web clients" ); + + Serial.println ( " SKTINT........Interval between check for websocket messages if no client connected" ); + + Serial.println ( " IFSIGNAL......Frequency of signal injected for IF Sweep (External or Ref)" ); + + Serial.println ( " BPFCAL........Start SI4432 bandpass filter calibration - RXSweep mode only"); + + Serial.println (); +} + + +/* + * "CheckCommand" checks the serial input for any messages. If there is nothing + * to read, it returns "false". + * + * If there is something to be read, the complete line of data is read into a + * buffer. The read loop is set up to recognize either a return or newline character + * as the line terminator and handles the case where both are used (as is possible + * when using the Arduino IDE's Serial Monitor. The function also translates all + * alpha characters to upper case. + * + * Once the command is read, the buffer is passed to "ProcessCommand" which does + * whatever was requested. If the command is successfully executed, the function + * returns "true" and returns "false" if the command was not successful. + */ + +bool CheckCommand () +{ + +static char inBuff[80]; // Input buffer +char c = 0; // Just one character +static int16_t index = 0; // Index to the buffer + + if ( !Serial.available() ) // Any input? + return false; // Nope! + + while ( Serial.available () ) // While data to read + { + c = toupper ( Serial.read () ); // Get next character + Serial.print(c); // Echo + //Serial.printf("(%02X)", c); // debug + + if ( ( c == '\r' ) || ( c == '\n' ) ) // End of the line? + { + Serial.println(); + inBuff[index++] = '\0'; // Replace with a null + //Serial.printf("EOL = %s \n", inBuff); // debug + boolean result = ParseCommand ( inBuff ); // Process the command and return result + index = 0; + return result; + } + else // Not the end of the line + { + inBuff[index++] = c; // Save the character + inBuff[index+1] = '\0'; // Make next character a null + } + if (index >= 78) + { + Serial.println("Serial Buffer Overun"); + index = 0; + break; + } + } + + return false; +// return ParseCommand ( inBuff ); // Process the command and return result +} + + +/* + * "ParseCommand" Translates the ASCII command text into a numerical value based + * on the contents of the "msgTable" and separates the data portion of the message + * (if any) into the "dataBuff". + * + * The process is a bit clever! The user only need to enter wnough characters of the + * command to make it unique! For example, if the user simply entered the command as + * "D", that could be interpreted as meaning "DRIVE" or "DELAY" (based on the contents + * of the "msgTable" as I write this). But entering "DE" or "DR" is enough to distinguish + * between the two. + * + * If the command is not found or is not unique, appropriate error messages are + * diaplayed and the function returns false. + * + * If the command is identified uniquely, the "ProcessCommand" function is invoked + * to complete the processing. + */ + +bool ParseCommand ( char* inBuff ) // inBuff has full message +{ + int messageLength; // Length of entire message + int cmdLength; // Length of command string + uint8_t cmdNumber; // Command translated to a number + + int cmdCount = 0; // Number of commands matching the input + int inputIx = 0; // Input buffer index + int cmdIx = 0; // Command string index + int dataIx = 0; // Data string index + + char cmdString[10]; // Will contain the command string + char dataBuff[20]; // Will contain the data portion of the message + char fullCmd[20]; // Full command from table + + char c; // Just one character + + memset ( cmdString, 0, sizeof ( cmdString )); // Clear the command buffer + memset ( dataBuff, 0, sizeof ( dataBuff )); // Clear the data buffer + + messageLength = strlen ( inBuff ); // Length of input + + if ( messageLength == 0 ) + return false; + + while ( inputIx < messageLength ) + { + c = toupper ( inBuff[inputIx++] ); // Get a character + + if (( c != ' ' ) && ( c != ',' )) // If it's not a space or a comma + cmdString[cmdIx++] = c; // Add to commmand string + + else // If it is a space or comma + break; // On to the data part of the message + } + + while ( inputIx < messageLength ) + dataBuff[dataIx++] = inBuff[inputIx++]; // Copy rest of message to the data buffer + + cmdLength = strlen ( cmdString ); // Length of the command string + cmdNumber = MSG_NONE; // No message found so far + + for ( inputIx = 0; inputIx < msgCount; inputIx++ ) // Search the msgTable + { + if ( strncmp ( cmdString, msgTable[inputIx].Name, cmdLength ) == 0 ) + { + cmdNumber = msgTable[inputIx].ID; // Get translation + strcpy ( fullCmd, msgTable[inputIx].Name ); // And remember full command string + cmdCount++; // Found at least one match + + if ( cmdCount > 1 ) // But did we find more than one? + { + Serial.printf ( "'%s' Is ambiguous!\n", cmdString ); + return false; + } + } + } + + if ( cmdNumber != MSG_NONE ) + { +// Serial.print ( "Command: " ); Serial.print ( fullCmd ); +// Serial.print ( " = " ); Serial.println ( cmdNumber ); +// Serial.print ( "Data: " ); Serial.println ( dataBuff ); + + return ( ProcessCommand ( cmdNumber, dataBuff )); + } + + else + + Serial.printf ( "Command: '%s' not found!\n", cmdString ); + + return false; +} + + +/* + * "ProcessCommand" parses the input buffer appropriately for the command, which is + * (for now) a single letter in the 1st byte of the buffer. + * + * If the command is executed correctly, the function returns a "true" indication + * and returns "false" if some error occurred. + */ + +bool ProcessCommand ( uint8_t command, char* dataBuff ) +{ +uint16_t dataIx = 0; // Data buffer index +uint8_t dataLen; // Length of data buffer + +char tempBuff[40]; // Buffer for formatting responses +int16_t tempIx = 0; // tempBuff index + +char c = 0; // A single character from the input + +int16_t addr; // Si4432 register address + +int16_t tempValue; // Temporary value of something +int32_t tempFreq; // Temporary frequency +float tempFloat; + +uint32_t freq1; // Several other temporary frequencies +uint32_t freq2; // used in computing the center +uint32_t freq3; // frequency and span range +uint32_t freq4; +int32_t signedFreq; + +int16_t attenuation; // Attenuation setting for the PE4302 + +uint8_t driveLevel[] = { 1, 2, 5, 8, 11, 14, 17, 20 }; // Drive level to dB translation +uint8_t fTranslate[7] = { 30, 15, 10, 4, 3, 2, 1 }; // GPIO2 frequency table + +bool agcOn; // Used by "GetPreampGain" +uint8_t reg69; // Ditto + + dataLen = strlen ( dataBuff ); // Length of the data string + +// Serial.print ( "Command: " ); Serial.println ( command ); +// Serial.print ( "Data: " ); Serial.println ( dataBuff ); + + +/* + * The original code had a plethora of "if" statements to try to figure out what + * command was entered; modified to a "switch" statement in Version 1.7. + */ + + switch ( command ) + { + + +/* + * The marker command is a bit more complicated than most. The format of the command + * is: + * + * MARKER # ACTION + * + * where '#' is the marker number (1 to 4) and 'ACTION' is one of the following: + * + * ENABLE Turns it on + * DISABLE Turns it off + * WHITE Set the color to white + * RED Set the color to red + * BLUE etc. + * GREEN + * YELLOW + * ORANGE + * + * Only the first letter of the 'ACTION' is necessary, so the command "MARKER 2 R" + * would set the color of marker #2 to red. + */ + + case MSG_MARKER: // Set marker status or color + if ( dataLen < 1 ) // Less than 1; no number specified + { + Serial.println ( "No marker number specified" ); + return false; + } + + if ( !isDigit ( dataBuff[0] )) // Should be the marker number + { + Serial.println ( "No marker number specified" ); + return false; + } + + if ( dataLen < 3 ) // Has to be at least 3 characters + { + Serial.println ( "No marker action specified" ); + return false; + } + + tempValue = dataBuff[0] - 0x30 - 1; // Quick atoi conversion + + if ( tempValue >= MARKER_COUNT ) // Range check + { + Serial.printf ( "Illegal marker number: %i\n", tempValue + 1); + return false; + } + + if ( UpdateMarker ( tempValue, dataBuff[2] )) // Update the status + return true; + + else + { + Serial.printf ( "Illegal marker action: %s\n", + &dataBuff[2] ); + return false; + } +/* + * The "START" command sets the scan START frequency. and the "STOP" command + * sets the scan STOP frequency. These are fairly straightforward! + */ + + case MSG_START: // Set the start frequency + if ( dataLen != 0 ) // Frequency specified? + { + tempFreq = ParseFrequency ( dataBuff ); // Then get the new frequency + SetSweepStart ( tempFreq ); // Set it + RedrawHisto(); + //DisplayInfo (); // Restart scan? + } + + Serial.printf ( "Scan start frequency: %s\n", + FormatFrequency ( GetSweepStart () )); + + return true; + + + case MSG_STOP: // Set the stop frequency + if ( dataLen != 0 ) // Frequency specified? + { + tempFreq = ParseFrequency ( dataBuff ); // Get new frequency + SetSweepStop ( tempFreq ); // And set it + RedrawHisto (); + } + + Serial.printf ( "Scan stop frequency: %s\n", + FormatFrequency ( GetSweepStop () )); + + return true; + + +/* + * When the user sets the "CENTER" frequency, we're also going to set the "SPAN". But, + * it's a bit tricky! + * + * The basic assumption is that we will set the "SPAN" equal to the selected "CENTER" + * frequency. But if the "SPAN" limits would cause the "START" frequency to go negative + * or cause the "STOP" frequency to exceed "STOP_MAX", the difference between the + * "CENTER" frequency and whichever limit is exceeded will become 1/2 of the "SPAN". + * + * Of course, setting the "CENTER" frequency at either one of the limits is going to + * result in a zero span. + */ + + case MSG_CENTER: // Set the center frequency + + if ( dataLen != 0 ) // Frequency specified? + { + tempFreq = ParseFrequency ( dataBuff ); // Yes, get new frequency + SetSweepCenter ( tempFreq, WIDE ); // Set it + RedrawHisto (); + } + + Serial.printf ( "Scan center frequency: %s\n", + FormatFrequency ( GetSweepCenter ( ) )); + + return true; + + + case MSG_SPAN: // Set the frequency span + if ( dataLen != 0 ) // Frequency specified? + { + tempFreq = ParseFrequency ( dataBuff ); // Yes, get new frequency + SetSweepSpan ( tempFreq ); // and set it + RedrawHisto (); + } + + Serial.printf ( "Sweep frequency span: %s\n", + FormatFrequency ( GetSweepSpan ( ) )); + + return true; + + + case MSG_FOCUS: // Set single frequency mode + if ( dataLen != 0 ) // Frequency specified? + { + tempFreq = ParseFrequency ( dataBuff ); // Yes, get new frequency + SetSweepCenter ( tempFreq, NARROW ); // Set it + RedrawHisto (); + } + + Serial.printf ( "Scan center frequency: %s\n", + FormatFrequency ( GetSweepCenter () )); + + return true; + +/* + * The "GRID" command sets the decibel value for the top grid line. + * + * If no number is entered, we report the current setting. + */ + + case MSG_GRID: + if ( dataLen != 0 ) // Value specified? + { + tempValue = atoi ( dataBuff ); // Yes, get grid reference value + SetRefLevel ( tempValue ); // Set it + DisplayInfo (); // Re display the scan + } + + Serial.printf ( "Grid reference value: %ddB\n", setting.MaxGrid ); + return true; + + +/* + * The "SCALE" command sets the decibel value spacing for each horizontal line on the grid. + * Unlike the touch-screen equivalent command, here you can set any value you like as opposed + * to chosing from a list of standard settings. + * + * If no number is entered, we report the current setting. + */ + + case MSG_SCALE: + if ( dataLen != 0 ) // Value specified? + { + tempValue = atoi ( dataBuff ); // Yes, get scale setting + SetPowerGrid ( tempValue ); // Set it + DisplayInfo (); // Re display the scan + } + + Serial.printf ( "Grid scale: %ddB/div\n", setting.PowerGrid ); + return true; + + +/* + * The "PREAMP" command sets the receiver Si4432 preamp gain. The way this works is pretty + * strange and is explained in A440, but not very clearly! + * + * There are two ranges; low and high. The low range values are 5dB to 29dB in 3dB steps. + * The high range goes from 25dB to 49dB in 3dB steps. Note there is some overlap! You can + * also specify "AUTO" mode which turns on the AGC in the module. + * + * Rather than go through the hassle of figuring out if the value entered by the user is + * one of the specific values allowed, we will allow the user to enter any value between + * 5dB and 49dB and if it's not one of the legal values, we will use the next lower value. + * + * Note the ranges overlap, so if the requested value is greater than 23dB, we'll just use + * the high range (in other words, we won't use 26dB or 29dB). + */ + + case MSG_PREAMP: + if ( dataLen != 0 ) // Any data? + { + if ( dataBuff[0] == 'A' ) // "AUTO" mode requested? + tempValue = AGC_ON; // Turn on the AGC + + else if ( isDigit ( dataBuff[0] )) // It should be a number + tempValue = atoi ( dataBuff ); // Convert it to a number + + SetPreampGain ( tempValue ); // Set the new value + } // End of if ( dataLen != 0 ) + + tempValue = GetPreampGain ( &agcOn, ®69 ); + + if ( agcOn ) // If AGC is on + Serial.println ( "Preamp AGC is on" ); + else // No AGC, report real gain + Serial.printf ( "Preamp gain is %ddB\n", tempValue ); + + // pushSettings (); + + return true; + + +/* + * The "DRIVE" command sets the local oscillator "drive" level. + */ + case MSG_DRIVE: + if ( dataLen != 0 ) // Drive level specified? + { + tempValue = atoi ( dataBuff ); // Yes, convert to a number + + if (( tempValue < MIN_DRIVE ) || ( tempValue > MAX_DRIVE )) + Serial.printf ( "Invalid drive specification: %d\n", tempValue ); + else // Set the transmitter drive level + SetLoDrive ( tempValue ); // "SetLoDrive" saves the settings + } + + Serial.printf ( "Drive: %d (+%udBm)\n", setting.Drive, driveLevel[setting.Drive] ); + + // pushSettings (); + + return true; + +/* + * Signal Generator On/Off commands + * + */ + case MSG_SGON: + SetSGState(1); + return true; + + case MSG_SGOFF: + SetSGState(0); + return true; + + case MSG_SGFREQ: + if ( dataLen != 0 ) // Frequency specified? + { + freq1 = ParseFrequency ( dataBuff ); // Yes, get new frequency + SetSGFreq (freq1); + } + Serial.printf ( "Signal Generator frequency: %i\n", FormatFrequency ( freq1 )); + return true; + + + + +/* + * The "SGLODRIVE" command sets the local oscillator "drive" level in signal generator mode. + */ + case MSG_SG_LO_DRIVE: + if ( dataLen != 0 ) // Drive level specified? + { + tempValue = atoi ( dataBuff ); // Yes, convert to a number + + if (( tempValue < MIN_DRIVE ) || ( tempValue > MAX_DRIVE )) + Serial.printf ( "Invalid LO drive: %d\n", tempValue ); + else // Set the transmitter drive level + SetSGLoDrive ( tempValue ); // "SetSGLoDrive" saves the settings + } + + Serial.printf ( "SigGen LO Drive: %d (+%udBm)\n", sigGenSetting.LO_Drive, driveLevel[sigGenSetting.LO_Drive] ); + + //pushSettings (); + + return true; + +/* + * The "SGRXDRIVE" command sets the receiver (IF) oscillator "drive" level in signal generator mode. + */ + case MSG_SG_RX_DRIVE: + if ( dataLen != 0 ) // Drive level specified? + { + tempValue = atoi ( dataBuff ); // Yes, convert to a number + + if (( tempValue < MIN_DRIVE ) || ( tempValue > RX_SI4432_MAX_DRIVE )) + Serial.printf ( "Invalid RX drive: %d - (%d-%d)\n", tempValue, MIN_DRIVE, RX_SI4432_MAX_DRIVE ); + else // Set the transmitter drive level + SetSGRxDrive ( tempValue ); // "SetSGRxDrive" saves the settings + } + + Serial.printf ( "SigGen RX Drive: %d (+%udBm)\n", sigGenSetting.RX_Drive, driveLevel[sigGenSetting.RX_Drive] ); + + //pushSettings (); + + return true; + +/* + * Tracking Generator On/Off commands + * + */ + case MSG_TGON: + SetTracking(1); + return true; + + case MSG_TGOFF: + SetTracking(0); + return true; + + +#ifdef TG_LO_INSTALLED + + case MSG_TGSIG: + SetTracking(2); + return true; + + +/* + * The "TGLODRIVE" command sets the local oscillator "drive" level in tracking generator mode. + * (only valid if two SI4432 used for the tracking generator) + */ + case MSG_TG_LO_DRIVE: + if ( dataLen != 0 ) // Drive level specified? + { + tempValue = atoi ( dataBuff ); // Yes, convert to a number + + if (( tempValue < MIN_DRIVE ) || ( tempValue > MAX_DRIVE )) + Serial.printf ( "Invalid drive specification: %d\n", tempValue ); + else // Set the transmitter drive level + SetTGLoDrive ( tempValue ); // "SetTGLoDrive" saves the settings + } + + Serial.printf ( "Tracking Generator LO Drive: %d (+%udBm)\n", trackGenSetting.LO_Drive, driveLevel[trackGenSetting.LO_Drive] ); + + //pushSettings (); + + return true; +#endif + + +#ifdef TG_IF_INSTALLED +/* + * The "TGRXDRIVE" command sets the receiver (IF) oscillator "drive" level in tracking generator mode. + */ + case MSG_TG_IF_DRIVE: + if ( dataLen != 0 ) // Drive level specified? + { + tempValue = atoi ( dataBuff ); // Yes, convert to a number + + if (( tempValue < MIN_DRIVE ) || ( tempValue > MAX_DRIVE )) + Serial.printf ( "Invalid drive specification: %d\n", tempValue ); + else // Set the transmitter drive level + SetTGIfDrive ( tempValue ); // "SetTGLoDrive" saves the settings + } + + Serial.printf ( "Tracking Generator IF Drive: %d (+%udBm)\n", trackGenSetting.IF_Drive, driveLevel[trackGenSetting.IF_Drive] ); + + //pushSettings (); + + return true; +#endif + + case MSG_SAVE: // Save the scan configuration + if ( dataLen != 0 ) // Location specified? + { + Save ( atoi ( dataBuff )); // Save the settings + return true; + } + + else + { + Serial.println ( "No save location specified!" ); + return false; + } + + + case MSG_RECALL: // Retrieve saved scan configuration + if ( dataLen != 0 ) // Location specified? + { + Recall ( atoi ( dataBuff )); // Save the settings + return true; + } + + else + { + Serial.println ( "No save location specified!" ); + return false; + } + + +/* + * "FREQ" - Get or set the frequency for selected VFO + */ + + case MSG_VFO_FREQ: + if ( dataLen != 0 ) // Frequency specified? + { + freq1 = ParseFrequency ( dataBuff ); // Yes, get new frequency + + if ( VFO == RX_4432 ) // Receiver? + rcvr.SetFrequency ( freq1 ); + + if ( VFO == TX_4432 ) // Transmitter? + xmit.SetFrequency ( freq1 ); +#ifdef TG_IF_INSTALLED + if ( VFO == TGIF_4432 ) // Track generator IF? + tg_if.SetFrequency ( freq1 ); +#endif +#ifdef TG_LO_INSTALLED + if ( VFO == TGLO_4432 ) // Track generator IF? + tg_lo.SetFrequency ( freq1 ); +#endif + } + + if ( VFO == RX_4432 ) // Receiver? + Serial.printf ( "RX frequency: %s\n", FormatFrequency ( rcvr.GetFrequency() )); + + if ( VFO == TX_4432 ) // Local Oscillator? + Serial.printf( "LO frequency: %s\n", FormatFrequency ( xmit.GetFrequency() )); +#ifdef TG_IF_INSTALLED + if ( VFO == TGIF_4432 ) // Local Oscillator? + Serial.printf( "TG IF frequency: %s\n", FormatFrequency ( tg_if.GetFrequency() )); +#endif +#ifdef TG_LO_INSTALLED + if ( VFO == TGLO_4432 ) // Local Oscillator? + Serial.printf( "TG LO frequency: %s\n", FormatFrequency ( tg_lo.GetFrequency() )); +#endif + return true; + + +/* + * The "ATTEN" command sets the PE4302 attenuation + */ + + case MSG_ATTEN: // Set attenuation + if ( dataLen != 0 ) // Anything entered? + { + attenuation = atoi ( dataBuff ); // Get the value + + if (( attenuation < 0 ) + || ( attenuation > PE4302_MAX )) // Range check + { + Serial.printf ( "Illegal attenuator setting: %ddB\n", attenuation ); + return false; + } + + SetAttenuation ( attenuation ); + } + + Serial.printf ( "Attenuator setting: %ddB\n", setting.Attenuate ); + + // pushSettings (); + + return true; + + + case MSG_HELP: // "HELP" or "?" + ShowMenu (); + return true; + + + case MSG_STEPS: // Set number of scan points + if ( dataLen != 0 ) // Anything entered? + steps = atoi ( &dataBuff[dataIx] ); + + Serial.print ( "Steps: " ); + Serial.println ( steps ); + return true; + + +/* + * The "DELAY" command sets the timestep in uS. + */ + + case MSG_DELAY: // Set the timestep in uS, currently 2000 + if ( dataLen != 0 ) // Anything entered? + { + delaytime = atol ( dataBuff ); // Yes, set new delay time + DisplayInfo (); + } + + Serial.printf ( "Time per scan step = %uuS\n", delaytime ); + return true; + +/* + * The "OFFDEL" command sets the timestep in uS for tuning using offsets. + */ + + case MSG_OFFDELAY: // Set the timestep in uS, currently 2000 + if ( dataLen != 0 ) // Anything entered? + { + offsetDelayTime = atol ( dataBuff ); // Yes, set new delay time + DisplayInfo (); + } + + Serial.printf ( "Offset delay time per bandscope step = %uuS\n", offsetDelayTime ); + return true; + + +/* + * The "VFO" command sets the active VFO + */ + + case MSG_VFO: + if ( dataLen != 0 ) // Anything entered? + { + if ( dataBuff[0] == 'R' ) // Receiver? + VFO = RX_4432; + + else if (( dataBuff[0] == 'L' ) + || ( dataBuff[0] == 'T' )) // Transmitter? + VFO = TX_4432; + +#ifdef TG_IF_INSTALLED + else if ( dataBuff[0] == 'I' ) // track gen IF + VFO = TGIF_4432; +#endif +#ifdef TG_LO_INSTALLED + else if ( dataBuff[0] == 'G' ) // track gen LO + VFO = TGLO_4432; +#endif + else // Illegal entry + { + VFO = RX_4432; // No, default to receiver + Serial.println ( "Illegal VFO specification! "); + } + } + + Serial.print ( "Selected VFO is: " ); // Confirm setting + + switch ( VFO ) + { + case RX_4432: // Receiver + Serial.printf ( " Receiver (%u)\n", VFO ); + break; + case TX_4432: // Transmitter (LO) + Serial.printf ( " Local Oscillator (%u)\n", VFO ); + break; + case TGIF_4432: // Transmitter (LO) + Serial.printf ( " Tracking Generator IF (%u)\n", VFO ); + break; + case TGLO_4432: // Transmitter (LO) + Serial.printf ( " Tracking Generator LO (%u)\n", VFO ); + break; + + } + return true; + + +/* + * The "RBW" command sets the resolution bandwidth (RBW). + * + * Entering "RBW A" (or "RBW AUTO") puts the RBW into automatic mode. + */ + + case MSG_RBW: + if ( dataLen != 0 ) // Anything entered? + { + if ( dataBuff[0] == 'A' ) // Auto RBW requested? + bandwidth = 0; // Set bandwidth to '0' + + else if ( isDigit ( dataBuff[0] )) // Was a number entered? + { + bandwidth = atof ( dataBuff ); // Yes, then get entered value + + if ( bandwidth < 2.7 ) + bandwidth = 2.7; + } + +// Serial.print ( "RBW: " ); Serial.println ( bandwidth ); + +// setting.Bandwidth10 = (int) ( bandwidth * 10 ); // Here also + + SetRBW ( (int) ( bandwidth * 10 )); + } + + Serial.print ( "RBW = " ); + + if ( setting.Bandwidth10 == 0 ) + Serial.printf ( "AUTO (%.1fHKz)\n", bandwidth ); + + else + Serial.printf ( "%.1fHKz\n", bandwidth ); + + return true; // End of 'W' command + + + case MSG_TRACES: // Only works on 'GAIN' trace for now + if ( dataLen != 0 ) // Anything entered? + { + if ( dataBuff[0] == 'G' ) // Gain trace requested? + { + setting.ShowGain = !setting.ShowGain; // Toggle the setting + WriteSettings (); // Save setting + return true; // Processed successfully + } + + if ( dataBuff[0] == 'D' ); // Manin scan requested? + { + setting.ShowSweep = !setting.ShowSweep; // Toggle the setting + WriteSettings (); // Save setting + return true; // Processed successfully + } + } + + else // Nothing specified + { + Serial.println ( "No 'TRACES' item specified!" ); + return false; // Unsuccessful + } + + + case MSG_IF_FREQ: + if ( dataLen != 0 ) // Anything entered? + { + if ( dataBuff[0] == '+' ) // User wants to add an increment? + { + strcpy ( tempBuff, &dataBuff[1] ); // Copy the number to "tempBuff" + tempValue = 1; // We'll multiply by '+1' + } + + else if ( dataBuff[0] == '-' ) // User wants to add a decrement? + { + strcpy ( tempBuff, &dataBuff[1] ); // Copy the number to "tempBuff" + tempValue = -1; // We'll multiply by '1' + } + + else // No sign entered + { + strcpy ( tempBuff, dataBuff ); // Copy the number to "tempBuff" + tempValue = 0; // Indicate full frequency in the buffer + } + + tempFreq = ParseFrequency ( tempBuff ); // Convert frequency string to a number + + if ( tempValue != 0 ) // Increment or decrement? + tempFreq = ( tempFreq * tempValue ) + setting.IF_Freq; + + if ( !SetIFFrequency ( tempFreq )) // Returns result of range check + Serial.println ( "Requested IF frequency exceeds limits; not changed!" ); + } + + Serial.printf ( "IF Frequency set to: %s\n", FormatFrequency ( setting.IF_Freq )); + return true; + + +/* + * "REGDUMP" Print the registers for selected VFO + */ + + case MSG_REG_DUMP: + if ( VFO == RX_4432 ) // Receiver? + { + Serial.println ( "\nReceiver registers:\n" ); + rcvr.PrintRegs(); + } + + if ( VFO == TX_4432 ) // Transmitter? + { + Serial.println ( "\nTransmitter (LO) registers:\n" ); + xmit.PrintRegs(); + } +#ifdef TG_IF_INSTALLED + if ( VFO == TGIF_4432 ) // Track generator IF + { + Serial.println ( "\nTracking Generator IF registers:\n" ); + tg_if.PrintRegs(); + } +#endif +#ifdef TG_LO_INSTALLED + if ( VFO == TGLO_4432 ) // Track generator Lo + { + Serial.println ( "\nTracking Generator LO registers:\n" ); + tg_lo.PrintRegs(); + } +#endif + + + return true; + + +/* + * The "RSSI" and "QUIT" commands turn the displaying of the RSSI (Receiver Signal + * Strength Indicator) readings in the scan. + * + * Notice "showRSSI" gets set to '2'. I tried making it an option to only show one + * pass through the sweep, but somehow, the output starts mid-sweep. Until I figure + * that out, continuois is the onlt option. The hooks for that are still in the + * sweep loop. + */ + + case MSG_RSSI: + if ( dataLen != 0 ) // Anything entered? + { + if ( dataBuff[0] == 'C' ) // Continuous reporting requested? + showRSSI = 2; // Yes + + else if ( dataBuff[0] == '1' ) // Or just one time? + showRSSI = 1; // Set it + } + + else // No specification + showRSSI = 1; // Then the default is one time + + initSweep = true; // Restart the sweep + return true; + + + case MSG_QUIT: + showRSSI = 0; // Turn RSSI display off + return true; + + +/* + * The "REGISTER" command can read or write any of the Si4432 registers; it's pretty clever! + * + * The register designation and (optional) data to be written are in HEX. If you enter + * something like "REGISTER 2F", the response will be the contents of the specified register + * (in this case, "2F"). + * + * If you enter something like "REGISTER 69 2", it will write 0x02 into register 0x69. + * + * Note, the current setting of "VFO" ("VFO" command) must be either the transmitter + * or receiver modules. + */ + + case MSG_SET_REG: + tempBuff[0] = 0; // Start with null terminator + tempIx = 0; // And zero out the index; + + if (( VFO != RX_4432 ) && ( VFO != TX_4432 ) && ( VFO != TGIF_4432 ) && ( VFO != TGLO_4432 ) ) + { + Serial.print ( "Illegal Si4432 module specified; 'VFO' = " ); + Serial.println ( VFO ); + return false; + } + + if ( dataLen == 0 ) // Any data? + { + Serial.println ( "No register specified!" ); + return false; + } + + +/* + * Ok, there is some data there so let's extract the register specification. + */ + + while ( dataIx < dataLen ) + { + c = dataBuff[dataIx++]; // Next character from input + + if ((( c != ' ' ) && ( c != ',' )) && ( isHex ( c ))) + { + tempBuff[tempIx++] = c; // Copy to temporary buffer + tempBuff[tempIx] = 0; // Add null terminator + } + + else if (( c != ' ' ) || ( c != ',' )) // If it's a space or a comma + break; // On to the next step + + if ( !isHex ( c )) // If not a valid hex digit + { + Serial.printf ( "'%c' is not a hexadecimal digit!\n", c ); + return false; + } + } + + addr = xtoi ( tempBuff ) & 0xFF; // Convert buffer to address + + tempIx = 0; // Start over for data if any + tempBuff[0] = 0; // Null terminate the buffer + + +/* + * If we got this far, we have a register address. Now we need to determine whether + * or not the user also specified a value to set in that register or if they just + * want a readout of what's in the register. + */ + + while ( dataIx < dataLen ) + { + c = dataBuff[dataIx++]; // Next character from input + + if ( !isHex ( c )) // Check for non hexadecimal digit + { + Serial.printf ( "'%c' is not a hexadecimal digit!\n", c ); + return false; + } + + tempBuff[tempIx++] = c; // Copy to other buffer + tempBuff[tempIx] = 0; // Null terminator + } + + if ( strlen ( tempBuff ) > 0 ) // If something in the buffer + { + tempValue = xtoi ( tempBuff ) & 0xFF; // Convert it to a hex number + + if ( VFO == RX_4432 ) // Receiver? + rcvr.WriteByte ( addr, tempValue ); // Send data to the device + + if ( VFO == TX_4432 ) // Transmitter? + xmit.WriteByte ( addr, tempValue ); // Send data to the device + +#ifdef TG_IF_INSTALLED + if ( VFO == TGIF_4432 ) // Track gen IF + tg_if.WriteByte ( addr, tempValue ); // Send data to the device +#endif +#ifdef TG_LO_INSTALLED + if ( VFO == TGLO_4432 ) // Track gen LO + tg_lo.WriteByte ( addr, tempValue ); // Send data to the device +#endif +} + + +/* + * So far, so good! When we get here, either the user just wanted to see what's in + * the register or we have already changed it. Now just report the contents of the + * selected register. + */ + + if ( VFO == RX_4432 ) // Receiver? + Serial.printf ( "RX Reg[0x%02X] = 0x%02X\n", addr, rcvr.ReadByte ( addr )); + + else if ( VFO == TX_4432 ) // Transmitter? + Serial.printf ( "TX Reg[0x%02X] = 0x%02X\n", addr, xmit.ReadByte ( addr )); +#ifdef TG_IF_INSTALLED + else if ( VFO == TGIF_4432 ) // Transmitter? + Serial.printf ( "TGIF Reg[0x%02X] = 0x%02X\n", addr, tg_if.ReadByte ( addr )); +#endif +#ifdef TG_LO_INSTALLED + else if ( VFO == TGLO_4432 ) // Transmitter? + Serial.printf ( "TGLO Reg[0x%02X] = 0x%02X\n", addr, tg_lo.ReadByte ( addr )); +#endif + + return true; + + +/* + * The "TUNE" comand allows the use to specify the crystal load capacitance for the + * selected VFO. This teaks the frequency. See A440 and the TinySA documentation for + * info on how it really works. + * + * We need to force the SI4432 to recalibrate the PLL after each tune by putting it into READY mode + * + */ + + case MSG_TUNE: + if ( dataLen != 0 ) // Value specified? + { + tempValue = xtoi ( dataBuff ); // Yes, get new frequency (in hex) + + if ( VFO == RX_4432 ) // Receiver? + { + rcvr.Tune ( tempValue ); // Tune it + config.RX_capacitance = tempValue; // And save it + } + + if ( VFO == TX_4432 ) // Transmitter? + { + xmit.Tune ( tempValue ); // Tune it + config.TX_capacitance = tempValue; // And save it + } +#ifdef TG_IF_INSTALLED + if ( VFO == TGIF_4432 ) // Trackgen IF + { + tg_if.Tune ( tempValue ); // Tune it + config.tgIF_capacitance = tempValue; // And save it + } +#endif +#ifdef TG_LO_INSTALLED + if ( VFO == TGLO_4432 ) // Track Gen LO + { + tg_lo.Tune ( tempValue ); // Tune it + config.tgLO_capacitance = tempValue; // And save it + } +#endif + } + + if ( VFO == RX_4432 ) // Receiver? + { + tempValue = rcvr.ReadByte ( REG_COLC ); // Get current setting + Serial.printf ( "RX capacitance: 0x%2X\n", tempValue ); + } + + if ( VFO == TX_4432 ) // Local Oscillator? + { + tempValue = xmit.ReadByte ( REG_COLC ); // Get current setting + Serial.printf ( "TX capacitance: 0x%2X\n", tempValue ); + } + +#ifdef TG_IF_INSTALLED + if ( VFO == TGIF_4432 ) // Tracking gen IF + { + tempValue = tg_if.ReadByte ( REG_COLC ); // Get current setting + Serial.printf ( "TGIF capacitance: 0x%2X\n", tempValue ); + } +#endif +#ifdef TG_LO_INSTALLED + if ( VFO == TGLO_4432 ) // Tracking gen LO + { + tempValue = tg_lo.ReadByte ( REG_COLC ); // Get current setting + Serial.printf ( "TGLO capacitance: 0x%2X\n", tempValue ); + } +#endif + + return true; + + +/* + * The "REF_FREQ" ("MSG_GPIO2") command sets or gets the current frequency setting for + * the GPIO2 calibration frequency for the transmitter Si4432. + * + * The legal values are: + * + * 30MHz, 15MHz, 10MHz, 4MHz, 3MHz, 2MHz, 1MHz + * + * WA2FZW - For whatever reason, when a value is entered and the register is set, the + * readback of register "REG_MCOC" is correct, but when no data is entered the readback + * is always zero. Needs further investigation! + */ + + case MSG_GPIO2: + if ( dataLen != 0 ) // Value specified? + { + if ( dataBuff[0] == 'O' ) // Turn it off? + tempIx = 7; // This will do it! + else + { + tempValue = atoi ( dataBuff ); // Yes, get new frequency + + for ( tempIx = 0; tempIx < 7; tempIx++ ) // Loop through the table + if ( tempValue == fTranslate[tempIx] ) // A match? + break; // Get out of the loop + + if ( tempIx > 6 ) // Illegal entry + { + Serial.printf ( "'%u' is an illegal GPIO2 frequency setting!\n", tempValue ); + return false; + } + } + SetRefOutput ( tempIx ); // Set the frequency + } + + tempValue = xmit.ReadByte ( REG_GPIO2 ); // Read the GPIO2 register + + if ( tempValue == 0x1F ) + Serial.println ( "Transmitter GPIO2 reference frequency is off!" ); + + else + { + tempValue = xmit.ReadByte ( REG_MCOC ); // Read the clock register + Serial.printf ( "Transmitter GPIO2 reference frequency: %uMHz\n", fTranslate[tempValue] ); + } + + return true; + + + case MSG_ACT_PWR: // Calibrate the indicated power level + if ( dataLen != 0 ) // Value specified? + { + tempValue = atof ( dataBuff ); // Yes, get new power reading + RequestSetPowerLevel ( tempValue ); + Serial.printf ( "Indicated power set to: %f\n", tempValue ); + return true; + } + + else + { + Serial.println ( "No indicated power specified!" ); + return false; + } + + case MSG_WIFI_UPDATE: // Set WiFi update target time + if ( dataLen != 0 ) // Value specified? + { + tempValue = atoi ( dataBuff ); // Yes, get new value in milliSeconds + // check valid + if ( (tempValue > 10) && (tempValue < 2000) ) + { + wiFiTargetTime = tempValue * 1000; // convert to microSeconds + Serial.printf ( "Target wifi Update time set to: %d\n", tempValue ); + return true; + } + else + { + Serial.println("Invalid - must be between 10ms and 2000ms"); + return false; + } + } + + else + { + Serial.printf ( "WiFi update target time is %ums\n", wiFiTargetTime/1000 ); + return false; + } + + + case MSG_WEBSKT_INTERVAL: // Set WiFi websocket interval time + if ( dataLen != 0 ) // Value specified? + { + tempValue = atoi ( dataBuff ); // Yes, get new value in microSeconds + // check valid + if ( (tempValue > 50) && (tempValue < 10000) ) + { + websocketInterval = tempValue; + Serial.printf ( "websocket poll interval set to: %uus\n", tempValue ); + return true; + } + else + { + Serial.println("Invalid - must be between 50us and 10,000us"); + return false; + } + } + + else + { + Serial.printf ( "Websocket poll interval is %uus\n", websocketInterval ); + return false; + } + + + case MSG_WIFI_POINTS: // Set WiFi chunk size + if ( dataLen != 0 ) // Value specified? + { + tempValue = atoi ( dataBuff ); // Yes, get new value + // check valid + if ( (tempValue > 10) && (tempValue < displayPoints * OVERLAP) ) + { + wiFiPoints = tempValue; + Serial.printf ( "Chunk size set to: %u\n", tempValue ); + return true; + } + else + { + Serial.printf("Invalid - must be between 10 and %u\n", displayPoints * OVERLAP); + return false; + } + } + + else + { + Serial.printf ( "Chunk size is %u\n", wiFiPoints ); + return false; + } + + + + case MSG_CONFIG: // Save configuration settings + WriteConfig (); + return true; + + case MSG_PAUSE: // Pause Scan (toggle) + paused = ! paused; + + if (paused) + Serial.println ( "Sweep paused" ); + + else + Serial.println ( "Sweep resumed" ); + + return true; + + case MSG_SWEEPLO: + setMode(SA_LOW_RANGE); + return true; + + case MSG_SIGLO: + setMode(SIG_GEN_LOW_RANGE); + return true; + + case MSG_BANDSCOPE: + setMode(BANDSCOPE); + return true; + + case MSG_IFSWEEP: + setMode(IF_SWEEP); + return true; + + case MSG_RX_SWEEP: + setMode(RX_SWEEP); + return true; + + case MSG_OTA: + setMode(OTA_UPDATE); + return true; + + case MSG_TGOFFSET: + if ( dataLen != 0 ) // Frequency specified? + { + signedFreq = ParseSignedFrequency ( dataBuff ); // Yes, get new frequency + SetTGOffset (signedFreq); + } + Serial.printf ( "Tracking Generator IF offset: %i\n", trackGenSetting.Offset ); // sort out signed version of format frequency!!!!!!!!!!!!!!!!!!! + return true; + + case MSG_TGFREQ: + if ( dataLen != 0 ) // Frequency specified? + { + freq1 = ParseFrequency ( dataBuff ); // Yes, get new frequency + SetTGFreq (freq1); + } + Serial.printf ( "Tracking Signal Generator frequency: %i\n", FormatFrequency ( trackGenSetting.Frequency )); + return true; + + +/* + * "FREQ" - Get or set the frequency for IF sweep injected signal + */ + + case MSG_IFSIGNAL: + if ( dataLen != 0 ) // Frequency specified? + { + freq1 = ParseFrequency ( dataBuff ); // Yes, get new frequency + SetIFsweepSigFreq (freq1); + } + Serial.printf ( "IF Sweep Signal frequency: %s\n", FormatFrequency ( GetIFsweepSigFreq() )); + return true; + + + case MSG_SPUR: + if ( dataLen != 0 ) // Anything entered? + { +// if ( dataBuff[0] == '1' ) // Turn Spur reduction on + if ( strcmp ( dataBuff, "ON" ) == 0 ) + { + SetSpur (1); + return true; // Processed successfully + } + + if ( strcmp ( dataBuff, "OFF" ) == 0 ) +// if ( dataBuff[0] == '0' ); // Turn Spur reduction off + { + SetSpur (0); + return true; // Processed successfully + } + + Serial.println( "Invalid argument for Spur command - use 1(on) or 0(off)" ); + return false; + } + + else // Nothing specified + { + Serial.printf ( "Spur reduction: %s\n", setting.Spur ? "ON" : "OFF" ); + return false; + } + + + case MSG_COLOURTEST: + if ( dataLen != 0 ) // Anything entered? + { + tempValue = atoi ( dataBuff ); // Yes, get new value + if ( ( tempValue >= 0 ) && (tempValue <= 65535) ) + { + colourTest = tempValue; + return true; + } + else // Nothing specified + { + Serial.println ( "Invalid Colour" ); + return false; + } + } + +/* + * The "WFMIN" command sets the min RSSI level for waterfall colouring. + * The value is entered in dBm so the value can be judged from the waterfall + * + * If no number is entered, we report the current setting. + */ + case MSG_WFMIN: + if ( dataLen != 0 ) // Value specified? + { + tempValue = atoi ( dataBuff ); // Yes, get grid reference value + SetWFMin ( tempValue ); // Set it + DisplayInfo (); // Re display the scan + } + + Serial.printf ( "Waterfall Minimum RSSI value: %i\n", setting.WaterfallMin ); + return true; + +/* + * The "WFGAIN" command sets the min RSSI level for waterfall colouring. + * + * If no number is entered, we report the current setting. + */ + case MSG_WFGAIN: + if ( dataLen != 0 ) // Value specified? + { + tempFloat = atof ( dataBuff ); // Yes, get grid reference value + SetWFGain ( tempFloat ); // Set it + Serial.printf("tempFloat = %4.2f\n", tempFloat); + DisplayInfo (); // Re display the scan + } + + Serial.printf ( "Waterfall colour gain: %4.2f\n", setting.WaterfallGain ); + return true; + +/* + * Set signal frequency fro RX Sweep + */ + case MSG_RXSIGNAL: + if ( dataLen != 0 ) // Frequency specified? + { + freq1 = ParseFrequency ( dataBuff ); // Yes, get new frequency + SetRXsweepSigFreq (freq1); + } + Serial.printf ( "RX Sweep Signal frequency: %s\n", FormatFrequency ( GetRXsweepSigFreq() )); + return true; + +/* + * Set external gain or attenuation + */ + case MSG_EXTGAIN: + if ( dataLen != 0 ) // Value specified? + { + tempFloat = atof ( dataBuff ); // Yes + SetExtGain( tempFloat ); + DisplayInfo (); // Re display the scan + } + Serial.printf ( "External gain: %4.1f\n", setting.ExternalGain ); + return true; + +/* + * Start calibration of the SI4432 bandpass filters (ie different RBW) + * Valid only if already in RX Sweep mode + */ + case MSG_BPFCAL: + return StartBpfCal (); + + + default: + return false; + } // End of "switch" +} // End of "ProcessCommand" + + +/* + * "ParseFrequency" expects a frequency in the "freqString". The format of the string + * is designed to be consistent with the way frequencies are entered using the touch + * screen menus. That is one can enter something like "100M" (or "100m") to specify + * 100MHz or "100k" (or "100K") to specify 100KHz. if you want to enter the + * value in pure Hertz, you can use commas (or dots) for clarity; for example, + * "100,000,000" would specify 100MHz. + */ + +uint32_t ParseFrequency ( char* freqString ) +{ +uint32_t freq = 0; // Used to build the frequency +uint8_t index = 0; // Input buffer index +uint8_t digit; // Next digit + + for ( index = 0; index < strlen ( freqString ); index++ ) + { + digit = freqString[index]; // Get a digit or something else + + if ( isDigit ( digit )) // If it is a number + freq = ( freq * 10 ) + ( digit - 0x30 ); // Cheap atoi conversion + + else if ( digit == 'M' ) + { + freq *= 1000000; // Make it MHz + return freq; // And send it back + } + + else if ( digit == 'K' ) + { + freq *= 1000; // Make it KHz + return freq; // And send it back + } + } + return freq; +} + + +int32_t ParseSignedFrequency ( char* freqString ) +{ +int32_t freq = 0; // Used to build the frequency +uint8_t index = 0; // Input buffer index +uint8_t digit; // Next digit +int32_t negative = 1; + + for ( index = 0; index < strlen ( freqString ); index++ ) + { + digit = freqString[index]; // Get a digit or something else + + if ( isDigit ( digit )) // If it is a number + freq = ( freq * 10 ) + ( digit - 0x30 ); // Cheap atoi conversion + + else if ( digit == 'M' ) + { + freq *= 1000000; // Make it MHz + return freq * negative; // And send it back + } + + else if ( digit == 'K' ) + { + freq *= 1000; // Make it KHz + return freq * negative; // And send it back + } + + else if ( digit == '-' ) + { + negative = -1; + } + } + return freq * negative; +} + +/* + * "FormatFrequency" breaks down the frequency into a format of "nnn,nnn,nnn" in the + * frequency buffer. + */ + +char* FormatFrequency ( uint32_t freq ) +{ + +uint32_t MHz; // MHz part of frequency +uint32_t KHz; // KHz part of frequency +uint32_t remainder; // Leftover pieces +static char freqBuff[20] = {0}; // The formatted frequency + + MHz = freq / 1000000; // Isolate MHz + remainder = freq % 1000000; // The remainder of the frequency + + KHz = remainder / 1000; // Isolate KHz + remainder = remainder % 1000; // Low order 3 digits + + if ( freq >= 1000000 ) + sprintf ( freqBuff, "%lu,%03lu,%03lu Hz", MHz, KHz, remainder ); + + else if ( freq >= 1000 ) + sprintf ( freqBuff, "%lu,%03lu Hz", KHz, remainder ); + + else + sprintf ( freqBuff, "%lu Hz", remainder ); + + return freqBuff; +} + + + +/* + * "FormatSignedFrequency" breaks down the frequency into a format of "nnn,nnn,nnn" in the + * frequency buffer. + */ + +char* FormatSignedFrequency ( int32_t freq ) +{ + +uint32_t uFreq; // turned positive +uint32_t MHz; // MHz part of frequency +uint32_t KHz; // KHz part of frequency +uint32_t remainder; // Leftover pieces +static char freqBuff[20] = {0}; // The formatted frequency +static char firstChar = ' '; + + if (freq < 0) + uFreq = -freq; + else + uFreq = freq; + + if (freq < 0) + firstChar = '-'; + + MHz = uFreq / 1000000; // Isolate MHz + remainder = uFreq % 1000000; // The remainder of the frequency + + KHz = remainder / 1000; // Isolate KHz + remainder = remainder % 1000; // Low order 3 digits + + if ( uFreq >= 1000000 ) + sprintf ( freqBuff, "%c%lu,%03lu,%03lu Hz", firstChar, MHz, KHz, remainder ); + + else if ( uFreq >= 1000 ) + sprintf ( freqBuff, "%c%lu,%03lu Hz", firstChar, KHz, remainder ); + + else + sprintf ( freqBuff, "%c%lu Hz", firstChar, remainder ); + + return freqBuff; +} + + + +/* + * "DisplayFrequency" breaks down the frequency into a format of "nnn,nnn,nnn" in the + * frequency buffer, with leading zeros and no unit + */ + +char* DisplayFrequency ( uint32_t freq ) +{ + +uint32_t MHz; // MHz part of frequency +uint32_t KHz; // KHz part of frequency +uint32_t remainder; // Leftover pieces +static char freqBuff[20] = {0}; // The formatted frequency + + MHz = freq / 1000000; // Isolate MHz + remainder = freq % 1000000; // The remainder of the frequency + + KHz = remainder / 1000; // Isolate KHz + remainder = remainder % 1000; // Low order 3 digits + + sprintf ( freqBuff, "%03lu,%03lu,%03lu", MHz, KHz, remainder ); + + return freqBuff; +} + +/* + * "xtoi()" works similar to "atoi()" except the string is assumed to be hex numbers. It will + * convert characters in the string as long as the next character in the string is a valid hex + * digit. In other words it stops converting if it sees a null or a ';', or any thing else that + * is not a valid hex digit. The only exception is the character "X" (or "x"), which is ignored. + * This allows it to correctly convert a string in the "0xNN..." format. + */ + +uint16_t xtoi ( char* hexString ) +{ +int ix; // Loop index +char c; // Single character +uint16_t answer = 0; // The answer + + for ( ix = 0; ix < strlen ( hexString ); ix++ ) + { + c = toupper ( hexString[ix] ); // Get next character + + if ( c == 'X' ) + continue; + + if ( isdigit ( c )) // Normal base 10 digit? + answer = ( answer * 16 ) + ( c - '0' ); // shift answer and add new number + + else if ( c >= 'A' && c <= 'F' ) // Valid hex digit? + answer = ( answer * 16 ) + ( c - 'A' + 10 ); // Shift answer and add new number + + else + break; + } + + return answer; +} + + +/* + * "isHex" returns true if the character is a valid hexadecimal digit or the letter 'X' + * (or 'x') as used in formatting a hexadecimal number as "0xNN". + */ + + bool isHex ( char c ) + { + char myC = toupper ( c ); // Make uppercase + + if (( myC >= '0' ) && ( myC <= '9' )) // standard digits + return true; + + if (( myC >= 'A' ) && ( myC <= 'F' )) // More digits + return true; + + if ( myC == 'X' ) // Special hex formatting + return true; + + return false; // If none of the above + } + + + +/* + * "UpdateMarker" changes the enabled/disabled status of a marker or its color. + * The "mkr" argument is the index to the marker, not its number. + * + * The "action" argument is (for now) a single character from the following list: + * + * [E]NABLE Turn the marker on + * [D]ISABLE Turn the marker off + * [W]HITE Set the color to white + * [R]ED Set the color to red + * [B]LUE etc. + * [G]REEN + * [Y]ELLOW + * [O]RANGE + */ + +bool UpdateMarker ( uint8_t mkr, char action ) +{ +uint8_t oldMkrStatus; + + oldMkrStatus = marker[mkr].Status (); // Remember marker'scurrent status + + switch ( action ) // What are we supposed to change + { + case 'E': // Enable the marker + marker[mkr].Enable (); + break; // Return success + + case 'D': // Disable the marker + marker[mkr].Disable (); + break; // Return success + + case 'W': // Set color to white + marker[mkr].Color ( WHITE ); + break; // Return success + + case 'R': // Set color to red + marker[mkr].Color ( RED ); + break; // Return success + + case 'B': // Set color to blue + marker[mkr].Color ( BLUE ); + break; // Return success + + case 'G': // Set color to green + marker[mkr].Color ( GREEN ); + break; // Return success + + case 'Y': // Set color to yellow + marker[mkr].Color ( YELLOW ); + break; // Return success + + case 'O': // Set color to orange + marker[mkr].Color ( ORANGE ); + break; // Return success + + default: // If none of the above + return false; // Illegal action/color + } + + +/* + * If we get here, the marker number and action were good. Now see if something + * actually changes and if so, update and save the "setting" structure. + */ + + if ( oldMkrStatus != marker[mkr].Status () ) + { + setting.MkrStatus[mkr] = marker[mkr].Status (); + WriteSettings (); + } + + return true; // Marker command processed ok +} + + + +/* + * "SetPreampGain" sets the preamp gain, which was added to the "setting" structure in + * Version 2.5. "GetPreampGain" reads it from the Si4432 and reports the current setting. + */ + +void SetPreampGain ( uint8_t gain ) +{ +uint8_t oldGain = setting.PreampGain; // Remember current setting + +// Serial.printf ( "Cmd.cpp - SetPreampGain %i \n", gain ); + + if (( gain == 0 ) || ( gain == 0x60 )) // AGC on request? + gain = 0x60; // That will be the setting + + else if ( gain < 25 ) // If < 25 use low range + { + gain = ( gain - 5 ) / 3; // Value for the register + gain &= PGAGAIN; // Just the low 4 bits + } + + else if ( gain >= 25 ) // If we found it above + { + gain = ( gain - 25 ) / 3; // Value for the register + gain &= PGAGAIN; // Just the low 4 bits + gain |= LNAGAIN; // Plus the high range bit + } + + setting.PreampGain = gain; // Set new value in the "setting" structure + rcvr.SetPreampGain ( gain ); // and in the receiver module + + if ( oldGain != setting.PreampGain ) // Did it actually change? + { + changedSetting = true; // Yes, need to update the display + WriteSettings (); // and save the "setting" structure + } +} + +uint8_t GetPreampGain ( bool* agc, uint8_t* reg ) +{ + uint8_t Reg69 = rcvr.GetPreampGain (); // Read the register + int lnaGain = 5; // Assume low gain + + if ( Reg69 & LNAGAIN ) // If LNA is turned on + lnaGain = 25; // Add 25 dB to total gain + + int pgaGain = ( Reg69 & PGAGAIN ) * 3; // 3dB per bit + + *agc = (bool) ( Reg69 & AGCEN ); // Set true if AGC is enabled + *reg = Reg69 & ( PGAGAIN | LNAGAIN ); + + return lnaGain + pgaGain; // Total gain +} + + +/* +* "SetRefOutput" - Sets the GPIO2 frequency for the LO Si4432 module +*/ + +void SetRefOutput ( int freq ) +{ +int oldFreq = setting.ReferenceOut; + + setting.ReferenceOut = freq; + xmit.SetPowerReference ( freq ); + + if ( oldFreq != setting.ReferenceOut ) + { + changedSetting = true; + WriteSettings (); + } +} + + +/* + * "SetRefLevel" - Sets the decibel level for the top line of the graph. + */ + +void SetRefLevel ( int ref ) +{ +int oldMin = setting.MinGrid; +int oldMax = setting.MaxGrid; + + setting.MaxGrid = ref; + setting.MinGrid = ref - yGrid * setting.PowerGrid; + + if (( oldMin != setting.MinGrid ) || ( oldMax != setting.MaxGrid )) + { + changedSetting = true; + WriteSettings (); + } +} + + +/* + * "SetGenerate" - Puts the unit into Signal Generator Mode + * Will be superceded by changing setting.mode + */ + +void SetGenerate ( int8_t g ) +{ +int oldG = setting.Generate; + + setting.Generate = g; + if (g) { + tinySA_mode = SIG_GEN_LOW_RANGE; + } else { + tinySA_mode = SA_LOW_RANGE; + } + + if ( oldG != setting.Generate ) + { + changedSetting = true; + WriteSettings (); + } +} + + +/* + * "SetTracking" - sets the Tracking Generator Mode + * 0 = off, 1 = track, 2 = sig gen + */ + +void SetTracking ( int8_t m ) +{ +int oldM = trackGenSetting.Mode; + + trackGenSetting.Mode = m; + + if ( oldM != trackGenSetting.Mode ) + { + changedSetting = true; + WriteTrackGenSettings (); + // pushSettings (); +// Serial.println("SetTracking - pushSettings"); + } +} + + +/* + * "SetPowerGrid" - Sets the dB/vertical divison on the grid + */ + +void SetPowerGrid ( int g ) +{ +int oldGrid = setting.PowerGrid; + + setting.PowerGrid = g; + setting.MinGrid = setting.MaxGrid - yGrid * g; + + if ( oldGrid != setting.PowerGrid ) + { + changedSetting = true; + WriteSettings (); + } +} + + +/* + * Set the IF frequency - M0WID addition + */ + +bool SetIFFrequency ( int32_t freq ) +{ +int32_t oldFreq = setting.IF_Freq; + +// Serial.print ( "SetIF: freq = " ); Serial.println ( freq ); + + if (( freq < MIN_IF_FREQ ) || ( freq > MAX_IF_FREQ )) // If out of limits + return false; // Don't change it + +// Serial.println ( "SetIF: Past range check" ); + + setting.IF_Freq = freq; + + if ( oldFreq != setting.IF_Freq ) + { + changedSetting = true; + WriteSettings (); + } + + return true; // Frequency was good +} + + +/* + * "SetAttenuation" - Modified by WA2FZW: + * + * "setting.Attenuate" is now stored as a positive number as displaying + * an attenuation of "-nn" (as was the case in the original software) + * would imply a gain value. All other places where it is used have been + * modified accordingly. + * + * Note, we don't actually set the attenuation in the PE4302 as that is done at + * the start of each sweep cycle. + */ + +void SetAttenuation ( int a ) +{ +int oldAtten = setting.Attenuate; + + setting.Attenuate = a; // Put value in the setting structure + + if ( oldAtten != setting.Attenuate ) + { + changedSetting = true; + WriteSettings (); + } +} + + +/* + * Set the external gain value + * +ve for gain, -ve for attenuation + */ +void SetExtGain ( double a ) +{ + setting.ExternalGain = a; + changedSetting = true; + WriteSettings (); + // pushSettings(); +} + +double GetExtGain (void ) +{ + return setting.ExternalGain; +} + + +/* + * "SetStorage" - Saves the results of a scan for comparison to an active one. + */ + +void SetStorage ( void ) +{ + for ( int i = 0; i < displayPoints; i++ ) + myStorage[i] = myData[i]; + + setting.ShowStorage = true; +} + + +/* + * "SetClearStorage" - Logically clears the saved scan data + */ + +void SetClearStorage ( void ) +{ + setting.ShowStorage = false; + setting.SubtractStorage = false; +} + + +/* + * "SetSubtractStorage" + */ + +void SetSubtractStorage ( void ) +{ + if ( !setting.ShowStorage ) + SetStorage (); + + setting.SubtractStorage = true; +} + + +/* + * "RequestSetPowerLevel" will be called from the menu or WiFi, possibly Serial to request + * the offset is calculated so that peak power measured in the sweep matches the input value + * the Request function sets the flag to initiate the calibration at the start of the next sweep + * using the input value. + */ + +void RequestSetPowerLevel ( float o ) +{ + actualPower = o; + setActualPowerRequested = true; +// Serial.printf ( "Request set Power Level %f \n", o ); +} + + +/* + * "SetPowerLevel" + * Adjusts displayed power to match the actual input signal power - calibration function + */ + +void SetPowerLevel ( double o ) +{ +double oldOffset = setting.LevelOffset; + + if ( o != 100.0 ) + setting.LevelOffset = ( o - ( (double)oldPeakLevel / 2.0 + setting.Attenuate - setting.ExternalGain - 120.0) ); // WA2FZW + else + setting.LevelOffset = 0.0; + +// Serial.printf ( "Peak level: %i, Actual: %f, Level offset: %f \n", +// oldPeakLevel, o, setting.LevelOffset ); + + if ( oldOffset != setting.LevelOffset ) + { + changedSetting = true; + RedrawHisto (); // Redraw labels and restart sweep with new settings + WriteSettings (); + // pushSettings(); +// Serial.println("Power level changed"); + } +} + + +/* + * "SetRBW" - Sets the resolution bandwidth in the "setting" structure. We don't + * bother to actually set it in the receiver Si4432 as that is done at the start + * of each scan cycle. + */ + +void SetRBW ( int bw ) +{ +int16_t oldRBW = setting.Bandwidth10; +int16_t tempBw; + + if ( bw == 0 ) // "AUTO" mode requested + tempBw = ( setting.ScanStop - setting.ScanStart ) / 30000; + + else + tempBw = bw; + + setting.Bandwidth10 = bw; + +// bandwidth = rcvr.SetRBW ( tempBw, &delaytime ); // Not needed as will be done at start of sweep + RedrawHisto (); // Redraw labels and restart sweep with new settings + + if ( setting.Bandwidth10 != oldRBW ) + { + changedSetting = true; + WriteSettings (); + } +} + + +/* + * "SetSpur" - Turns spurious signal supression on or off + * Also turns on the MIN trace which is needed for the spur reduction to be seen. + * Only turns the average trace off of it is still set to min + * Also turns off/on the dB trace + */ + +void SetSpur ( int v ) +{ +int oldSpur = setting.Spur; + + setting.Spur = v; + + if (setting.Spur) { + setting.Average = AV_MIN; + setting.ShowSweep = false; + } else { + if (setting.Average == AV_MIN) + setting.Average = AV_OFF; + setting.ShowSweep = true; + } + if ( oldSpur != setting.Spur ) + { + changedSetting = true; + WriteSettings (); + // pushSettings (); + } +} + + +/* + * "SetAverage" - Sets the number of readings to average for each display point + */ + +void SetAverage ( int v ) +{ +int oldAvg = setting.Average; + + setting.Average = v; + + if ( oldAvg != setting.Average ) + { + changedSetting = true; + WriteSettings (); + } +} + + +/* + * "SetLoDrive" sets the transmitter output level. + * + * The drive value can be from '0' to '7' and the output power will be set + * according to the following table (from page 39 of the Si4432 datasheet): + * + * 0 => +1dBm 4 => +11dBm + * 1 => +2dBm 5 => +14dBm + * 2 => +5dBm 6 => +17dBm + * 3 => +8dBm 7 => +20dBm + */ + +void SetLoDrive ( uint8_t level ) +{ +int oldLevel = setting.Drive; + + if (( level < 0 ) || ( level > 7 )) + return; + + else + { + setting.Drive = level; + xmit.SetDrive ( level ); // Set transmitter power level + + if ( oldLevel != setting.Drive ) + { + changedSetting = true; + WriteSettings (); + // pushSettings (); + } + } +} + + +/* Set SGFreq sets the signal generator Frequency + * Signal generator frequency may change frequently, so don't save the setting + * Settings are saved when leaving the mode + * parameter is in Hz + */ +bool SetSGFreq (uint32_t f) +{ + if ( ( f >= MIN_SIGLO_FREQ ) && ( f <= MAX_SIGLO_FREQ ) ) + { + sigGenSetting.Frequency = f; + return true; + } + else + return false; +} + + +/* + * Sig gen state is not saved - always want to start in off state + */ + +void SetSGState (uint16_t s) +{ + sigGenOutputOn = s; +} + +/* + * "SetSGLoDrive" sets the transmitter max output level in signal generator mode. + * + * The drive value can be from '0' to '7' and the output power will be set + * according to the following table (from page 39 of the Si4432 datasheet): + * + * 0 => +1dBm 4 => +11dBm + * 1 => +2dBm 5 => +14dBm + * 2 => +5dBm 6 => +17dBm + * 3 => +8dBm 7 => +20dBm + */ + +void SetSGLoDrive ( uint8_t level ) +{ +int oldLevel = sigGenSetting.LO_Drive; + + if (( level < 0 ) || ( level > 7 )) + return; + + else + { + sigGenSetting.LO_Drive = level; + + if (setting.Mode == SIG_GEN_LOW_RANGE) + xmit.SetDrive ( level ); // Set transmitter power level + + if ( oldLevel != sigGenSetting.LO_Drive ) + { + changedSetting = true; + WriteSigGenSettings (); + } + } +} + + + +/* + * "SetSGRxDrive" sets the receiver output level in signal generator mode + * + * The drive value can be from '0' to '7' and the output power will be set + * according to the following table (from page 39 of the Si4432 datasheet): + * + * 0 => +1dBm 4 => +11dBm + * 1 => +2dBm 5 => +14dBm + * 2 => +5dBm 6 => +17dBm + * 3 => +8dBm 7 => +20dBm + * Note the power limits of the B3555 SAW filter is 10dBm + * RX_SI4432_MAX_DRIVE is set in my_SA.h to suit the attenuator pad and losses + * in your build. + */ + +void SetSGRxDrive ( uint8_t level ) +{ +int oldLevel = sigGenSetting.RX_Drive; + + if (( level < 0 ) || ( level > RX_SI4432_MAX_DRIVE )) + return; + + else + { + sigGenSetting.RX_Drive = level; + if (setting.Mode == SIG_GEN_LOW_RANGE) + rcvr.SetDrive ( level ); // Set transmitter power level + + if ( oldLevel != sigGenSetting.RX_Drive ) + { + changedSetting = true; + WriteSigGenSettings (); + } + } +} + + +void SetSGPower ( int16_t dBm ) +{ + // Add algorithm to set attenuator and IF drive level + // initially limit to just set attenuator + + if ( dBm > sigGenSetting.Calibration ) + dBm = sigGenSetting.Calibration; + if ( dBm < (sigGenSetting.Calibration - ATTENUATOR_RANGE - RX_SI4432_MAX_DRIVE * 3) ) + dBm = sigGenSetting.Calibration - ATTENUATOR_RANGE; + + sigGenSetting.Power = dBm; + changedSetting = true; +} + +#ifdef TG_LO_INSTALLED +/* + * "SetTGLoDrive" sets the transmitter max output level for the tracking generator. + * + * The drive value can be from '0' to '7' and the output power will be set + * according to the following table (from page 39 of the Si4432 datasheet): + * + * 0 => +1dBm 4 => +11dBm + * 1 => +2dBm 5 => +14dBm + * 2 => +5dBm 6 => +17dBm + * 3 => +8dBm 7 => +20dBm + */ + +void SetTGLoDrive ( uint8_t level ) +{ +int oldLevel = trackGenSetting.LO_Drive; + + if (( level < 0 ) || ( level > 7 )) + return; + + else + { + trackGenSetting.LO_Drive = level; + + tg_lo.SetDrive ( level ); // Set transmitter power level + + if ( oldLevel != trackGenSetting.LO_Drive ) + { + changedSetting = true; + WriteTrackGenSettings (); + } + } +} +#endif + + +#ifdef TG_IF_INSTALLED +/* + * "SetTGIfDrive" sets the transmitter output level for the tracking generator IF. + * + * The drive value can be from '0' to '7' and the output power will be set + * according to the following table (from page 39 of the Si4432 datasheet): + * + * 0 => +1dBm 4 => +11dBm + * 1 => +2dBm 5 => +14dBm + * 2 => +5dBm 6 => +17dBm + * 3 => +8dBm 7 => +20dBm + */ + +void SetTGIfDrive ( uint8_t level ) +{ +int oldLevel = trackGenSetting.IF_Drive; + + if (( level < 0 ) || ( level > 7 )) + return; + + else + { + trackGenSetting.IF_Drive = level; + tg_if.SetDrive ( level ); // Set transmitter power level + + if ( oldLevel != trackGenSetting.IF_Drive ) + { + changedSetting = true; + WriteTrackGenSettings (); + // pushSettings (); + } + } +} +#endif + +#ifdef TG_IF_INSTALLED +void SetTGPower ( int16_t dBm ) +{ + // Add algorithm to set attenuator and IF drive level + // initially limit to just set drive levels + // TG Output = IF output - mixer loss - attenuator pads + int16_t pathLoss = 10; //6.9 mixer + 3 pad; + int16_t maxOut = 20 - pathLoss; + int16_t minOut = 1 - pathLoss; + + if (dBm > maxOut) + dBm = maxOut; + if (dBm < minOut) + dBm = minOut; + + uint8_t level = ( dBm + pathLoss + 1 ) / 3; + Serial.printf("Set TG power - req dBm %i - level set to %i\n", dBm, level); + + trackGenSetting.Power = dBm; // saved in SetTGIFDrive + SetTGIfDrive ( level ); +} +#endif + + +/* + * Set Tracking generator IF offset from main SA IF + * Ideally TG IF should be outside bandwidth of main RX + */ +bool SetTGOffset ( int32_t offset) +{ + // check valid + if ( (offset > -1000000 ) && (offset < 1000000 ) ) + trackGenSetting.Offset = offset; + changedSetting = true; + WriteTrackGenSettings (); +} + + +/* + * Get Tracking generator IF offset from main SA IF + */ +int32_t GetTGOffset ( ) +{ + return trackGenSetting.Offset; +} + +/* + * Set the Track Gen frequency - M0WID addition + */ + +bool SetTGFreq ( int32_t freq ) +{ +int32_t oldFreq = trackGenSetting.Frequency; + + if (( freq < MIN_SIGLO_FREQ ) || ( freq > MAX_SIGLO_FREQ )) // If out of limits + return false; // Don't change it + + trackGenSetting.Frequency = freq; + + if ( oldFreq != trackGenSetting.Frequency ) + { + changedSetting = true; + WriteTrackGenSettings (); + } + + return true; // Frequency was good +} + + +/* + * "SetSweepStart" and "GetSweepStart" were added in Version 2.3. They used to be in + * "switch" statements in "SetSweepFrequency" and "GetSweepFrequency". By making them + * separate function, they can be used by the "SetSweepCenter", "SetSweepSpan", etc. + * functions, thus eliminating a bunch of redundent code. + */ + +void SetSweepStart ( uint32_t startFreq ) +{ +uint32_t lastStart; // Old ssweep start frequency +uint32_t lastStop; // and old stop frequency + +// Serial.print ( "Requested start = " ); Serial.println ( FormatFrequency ( startFreq )); + + lastStart = setting.ScanStart; // Save the current start frequency + lastStop = setting.ScanStop; // and the current stop frequency + + if ( startFreq < START_MIN ) // Range check + startFreq = START_MIN; + + if ( startFreq > STOP_MAX ) // Range check + startFreq = STOP_MAX; + + if ( startFreq > setting.ScanStop ) // Start can't be more than current stop + setting.ScanStop = STOP_MAX; // So set stop at maximum + + setting.ScanStart = startFreq; // Set new start frequency + + if (( setting.ScanStart != lastStart ) + || ( setting.ScanStop != lastStop )) // Did it change? + { + changedSetting = true; // Yes it did + RedrawHisto (); // Redraw labels and restart sweep with new settings + WriteSettings (); // and save the setting structure + } +} + + +uint32_t GetSweepStart ( void ) +{ + return setting.ScanStart; +} + + +/* + * "SetSweepStop" and "GetSweepStop" were also added in Version 2.3. + */ + +void SetSweepStop ( uint32_t stopFreq ) +{ +uint32_t lastStart; // Old ssweep start frequency +uint32_t lastStop; // and old stop frequency + +// Serial.print ( "Requested stop = " ); Serial.println ( FormatFrequency ( stopFreq )); + + lastStart = setting.ScanStart; // Save the current start frequency + lastStop = setting.ScanStop; // and the current stop frequency + + if ( stopFreq < START_MIN ) // Range check + stopFreq = START_MIN; + + if ( stopFreq > STOP_MAX ) // Range check + stopFreq = STOP_MAX; + + if ( stopFreq < setting.ScanStart ) // Stop can't be less than current start + setting.ScanStart = START_MIN; // so set start to minimum frequency + + setting.ScanStop = stopFreq; // Set new stop frequency + + if (( setting.ScanStart != lastStart ) + || ( setting.ScanStop != lastStop )) // Did it change + { + changedSetting = true; // Yes it did + RedrawHisto (); // Redraw labels and restart sweep with new settings + WriteSettings (); // and save the setting structure + } +} + + +uint32_t GetSweepStop ( void ) +{ + return setting.ScanStop; +} + + +/* + * Specific start/stop frequency setting for IF Sweep mode + */ + +void SetIFsweepStart ( uint32_t startFreq ) +{ +uint32_t lastStart; // Old sweep start frequency +uint32_t lastStop; // and old stop frequency + +// Serial.print ( "Requested start = " ); Serial.println ( FormatFrequency ( startFreq )); + + lastStart = startFreq_IF; // Save the current start frequency + lastStop = stopFreq_IF; // and the current stop frequency + + if ( startFreq < IF_START_MIN ) // Range check + startFreq = IF_START_MIN; + + if ( startFreq > IF_STOP_MAX ) // Range check + startFreq = IF_STOP_MAX; + + if ( startFreq > stopFreq_IF ) // Start can't be more than current stop + stopFreq_IF = IF_STOP_MAX; // So set stop at maximum + + startFreq_IF = startFreq; // Set new start frequency + + if (( startFreq_IF != lastStart ) + || ( stopFreq_IF != lastStop )) // Did it change? + { + changedSetting = true; // Yes it did + RedrawHisto (); // Redraw labels and restart sweep with new settings +// WriteSettings (); // and save the setting structure + } +} + + +uint32_t GetIFsweepStart ( void ) +{ + return startFreq_IF; +} + + + + +void SetIFsweepStop ( uint32_t stopFreq ) +{ +uint32_t lastStart; // Old sweep start frequency +uint32_t lastStop; // and old stop frequency + +// Serial.print ( "Requested stop = " ); Serial.println ( FormatFrequency ( stopFreq )); + + lastStart = startFreq_IF; // Save the current start frequency + lastStop = stopFreq_IF; // and the current stop frequency + + if ( stopFreq < IF_START_MIN ) // Range check + stopFreq = IF_START_MIN; + + if ( stopFreq > IF_STOP_MAX ) // Range check + stopFreq = IF_STOP_MAX; + + if ( stopFreq < startFreq_IF ) // Stop can't be less than current start + startFreq_IF = IF_START_MIN; // so set start to minimum frequency + + stopFreq_IF = stopFreq; // Set new stop frequency + + if (( startFreq_IF != lastStart ) + || ( stopFreq_IF != lastStop )) // Did it change? + { + changedSetting = true; // Yes it did + RedrawHisto (); // Redraw labels and restart sweep with new settings +// WriteSettings (); // and save the setting structure + } +} + + +uint32_t GetIFsweepStop ( void ) +{ + return stopFreq_IF; +} + + +void SetIFsweepSigFreq ( uint32_t sigFreq ) +{ +uint32_t lastSigFreq; + +// Serial.print ( "Requested if sweep signal = " ); Serial.println ( FormatFrequency ( sigFreq_IF )); + + lastSigFreq = sigFreq_IF; + +// check in a sensible range + if ( ( sigFreq < 1000000 ) || (sigFreq > 100000000 ) ) + Serial.println(" Out of range - must be between 1,000,000 and 100,000,000Hz"); + else + { + sigFreq_IF = sigFreq; + if ( sigFreq_IF != lastSigFreq ) + changedSetting = true; + } + + RedrawHisto (); // Redraw labels and restart sweep with new settings + +} + + +uint32_t GetIFsweepSigFreq ( void ) +{ + return sigFreq_IF; +} + + +/* + * Specific span setting for RX Sweep mode + * + * + * "SetSweepSpan" will maintain the previous center frequency and attempt to simply + * change the range of the scan. If the new limits cause the start frequency to go + * or the stop frequency to exceed "STOP_MAX", the range will be adjusted to maintain + * an equal range on both sides of the center frequency. + */ + +void SetRXsweepSpan ( uint32_t spanRange ) +{ +uint32_t startFreq; // These are used in computing the +uint32_t stopFreq; // Scan limits +uint32_t centerFreq; // Current center frequency +uint32_t halfSpan; // Half of the frequency span + +uint32_t lastStart; // Old ssweep start frequency +uint32_t lastStop; // and old stop frequency + +// Serial.print ( "Requested span = " ); Serial.println ( FormatFrequency ( spanRange )); + + lastStart = startFreq_RX; // Save the current start frequency + lastStop = stopFreq_RX; // and the current stop frequency + + halfSpan = spanRange / 2; // Half the requested span + centerFreq = setting.IF_Freq; // Always the IF Freq for this mode + + startFreq = centerFreq - halfSpan; // New computed start frequency + stopFreq = centerFreq + halfSpan; // New computed stop frequency + + +/* + * Now we have to range check the computed start and stop frequencies and if either + * of those is out of bounds, we will adjust the span accordingly. We'll maintain + * the previous center frequency though. + */ + + if ( stopFreq > RX_STOP_MAX ) // Range check the stop frequency + { + stopFreq = RX_STOP_MAX; // Set the stop frequency at the upper limit + halfSpan = stopFreq - centerFreq; // Re-compute half span + startFreq = centerFreq - halfSpan; // and recompute start frequency + } + + if ( startFreq < RX_START_MIN ) // Range check the start frequency + { + startFreq = RX_START_MIN; // Set start at the limit + halfSpan = centerFreq - startFreq; // Re-compute half span + stopFreq = centerFreq + halfSpan; // and re-compute stop frequency + } + + startFreq = centerFreq - halfSpan; // Re-compute start and + stopFreq = centerFreq + halfSpan; // stop frequencies + +// Serial.print ( "Computed start = " ); Serial.println ( FormatFrequency ( startFreq )); +// Serial.print ( "Computed stop = " ); Serial.println ( FormatFrequency ( stopFreq )); +// Serial.print ( "Half span = " ); Serial.println ( FormatFrequency ( halfSpan )); + + startFreq_RX = startFreq; // Set new start frequency + stopFreq_RX = stopFreq; // Set new stop frequency + + if (( startFreq_RX != lastStart ) + || ( stopFreq_RX != lastStop )) // Did it change + { + changedSetting = true; // Yes it did + RedrawHisto (); // Redraw labels and restart sweep with new settings + } +} + + +uint32_t GetRXsweepSpan () +{ + return stopFreq_RX - startFreq_RX; +} + + +void SetRXsweepStart ( uint32_t startFreq ) +{ +uint32_t lastStart; // Old sweep start frequency +uint32_t lastStop; // and old stop frequency + +// Serial.print ( "Requested start = " ); Serial.println ( FormatFrequency ( startFreq )); + + lastStart = startFreq_RX; // Save the current start frequency + lastStop = stopFreq_RX; // and the current stop frequency + + if ( startFreq < RX_START_MIN ) // Range check + startFreq = RX_START_MIN; + + if ( startFreq > RX_STOP_MAX ) // Range check + startFreq = RX_STOP_MAX; + + if ( startFreq > stopFreq_RX ) // Start can't be more than current stop + stopFreq_RX = RX_STOP_MAX; // So set stop at maximum + + startFreq_RX = startFreq; // Set new start frequency + + if (( startFreq_RX != lastStart ) + || ( stopFreq_RX != lastStop )) // Did it change? + { + changedSetting = true; // Yes it did + RedrawHisto (); // Redraw labels and restart sweep with new settings +// WriteSettings (); // and save the setting structure + } +} + + +uint32_t GetRXsweepStart ( void ) +{ + return startFreq_RX; +} + + + + +void SetRXsweepStop ( uint32_t stopFreq ) +{ +uint32_t lastStart; // Old sweep start frequency +uint32_t lastStop; // and old stop frequency + +// Serial.print ( "Requested stop = " ); Serial.println ( FormatFrequency ( stopFreq )); + + lastStart = startFreq_RX; // Save the current start frequency + lastStop = stopFreq_RX; // and the current stop frequency + + if ( stopFreq < RX_START_MIN ) // Range check + stopFreq = RX_START_MIN; + + if ( stopFreq > RX_STOP_MAX ) // Range check + stopFreq = RX_STOP_MAX; + + if ( stopFreq < startFreq_RX ) // Stop can't be less than current start + startFreq_RX = RX_START_MIN; // so set start to minimum frequency + + stopFreq_RX = stopFreq; // Set new stop frequency + + if (( startFreq_RX != lastStart ) + || ( stopFreq_RX != lastStop )) // Did it change? + { + changedSetting = true; // Yes it did + RedrawHisto (); // Redraw labels and restart sweep with new settings +// WriteSettings (); // and save the setting structure + } +} + + +uint32_t GetRXsweepStop ( void ) +{ + return stopFreq_RX; +} + + +void SetRXsweepSigFreq ( uint32_t sigFreq ) +{ +uint32_t lastSigFreq; + +// Serial.print ( "Requested RX sweep signal = " ); Serial.println ( FormatFrequency ( sigFreq_RX )); + + lastSigFreq = sigFreq_RX; + +// check in a sensible range + if ( ( sigFreq < 1000000 ) || (sigFreq > 100000000 ) ) + Serial.println(" Out of range - must be between 1,000,000 and 100,000,000Hz"); + else + { + sigFreq_RX = sigFreq; + if ( sigFreq_RX != lastSigFreq ) + changedSetting = true; + } + + RedrawHisto (); // Redraw labels and restart sweep with new settings + +} + + +uint32_t GetRXsweepSigFreq ( void ) +{ + return sigFreq_RX; +} + + +/* + * start the bandpass filter calibration sequence + * must be in RX_Sweep mode + */ + boolean StartBpfCal ( void ) + { + if ( setting.Mode == RX_SWEEP ) + { + bpfCalibrate = true; + Serial.println("Starting bpf calibration"); + return true; + } + else + { + Serial.println("bpfCal only works in RX_SWEEP (RBW test) mode"); + return false; + } + } + +/* + * Specific start/stop frequency setting for Bandscope mode + */ + +void SetBandscopeStart ( uint32_t startFreq ) +{ +uint32_t lastStart; // Old sweep start frequency + +// Serial.print ( "Requested start = " ); Serial.println ( FormatFrequency ( startFreq )); + + lastStart = setting.BandscopeStart; // Save the current start frequency + + if ( startFreq < START_MIN ) // Range check + startFreq = START_MIN; + + if ( startFreq > STOP_MAX ) // Range check + startFreq = STOP_MAX; + + setting.BandscopeStart = startFreq; // Set new start frequency + + if ( setting.BandscopeStart != lastStart ) + { + changedSetting = true; // Yes it did + initSweep = true; + WriteSettings (); // and save the setting structure + } +} + + +/* + * "SetRBW" - Sets the resolution bandwidth in the "setting" structure. We don't + * bother to actually set it in the receiver Si4432 as that is done at the start + * of each scan cycle. + */ +void SetBandscopeRBW ( int bw ) +{ +int16_t oldRBW = setting.BandscopeRBW10; +int16_t tempBw = bw; + + setting.BandscopeRBW10 = bw; + + tempBw = rcvr.SetRBW ( bw, &delaytime, &bpfIndex ); + + if ( tempBw != oldRBW ) + { + changedSetting = true; + WriteSettings (); + } +} + + +uint32_t GetBandscopeStart ( void ) +{ + return setting.BandscopeStart; +} + + + +/* + * Specific start/stop frequency setting for Bandscope mode + */ +void SetBandscopeSpan ( uint32_t spanFreq ) +{ +uint32_t lastSpan; // Old sweep span + +// Serial.print ( "Requested bandscope span = " ); Serial.println ( FormatFrequency ( spanFreq )); + + lastSpan = setting.BandscopeSpan; // Save the current start frequency + + if ( spanFreq < 100000 ) // Range check + spanFreq = 100000; + + if ( spanFreq > 350000 ) // Range check + spanFreq = 350000; + + setting.BandscopeSpan = spanFreq; // Set new start frequency + + if ( setting.BandscopeSpan != lastSpan ) + { + changedSetting = true; // Yes it did + initSweep = true; +// RedrawHisto (); // Redraw labels and restart sweep with new settings +// WriteSettings (); // and save the setting structure + } +} + + +uint32_t GetBandscopeSpan ( void ) +{ + return setting.BandscopeSpan; +} + + +/* + * "SetBandscopeLevel" - Sets the decibel level for the top line of the graph in Bandscope mode. + */ + +void SetBandscopeLevel ( int ref ) +{ +int oldMin = setting.BandscopeMinGrid; +int oldMax = setting.BandscopeMaxGrid; + + setting.BandscopeMaxGrid = ref; + setting.BandscopeMinGrid = ref - yGrid * setting.PowerGrid; + + if (( oldMin != setting.BandscopeMinGrid ) || ( oldMax != setting.BandscopeMaxGrid )) + { + changedSetting = true; + WriteSettings (); + } +} + + + +/* + * When the user sets the "CENTER" frequency, we're also going to set the "SPAN". But, + * it's a bit tricky! + * + * The basic assumption is that we will set the "SPAN" equal to the selected "CENTER" + * frequency. But if the "SPAN" limits would cause the "START" frequency to go negative + * or cause the "STOP" frequency to exceed "STOP_MAX", the difference between the + * "CENTER" frequency and whichever limit is exceeded will become 1/2 of the "SPAN". + * + * Of course, setting the "CENTER" frequency at either one of the limits is going to + * result in a zero span. + */ + +void SetSweepCenter ( uint32_t freq, uint8_t span ) +{ +uint32_t startFreq; // These are used in computing the +uint32_t stopFreq; // Scan limits +uint32_t halfSpan; // Half of the frequency span + +uint32_t lastStart; // Old ssweep start frequency +uint32_t lastStop; // and old stop frequency + +// Serial.print ( "Requested center = " ); Serial.println ( FormatFrequency ( freq )); + + lastStart = setting.ScanStart; // Save the current start frequency + lastStop = setting.ScanStop; // and the current stop frequency + + +/* + * Range check the requested frequency against the sweep limits and adjust accordingly: + */ + + if ( freq > STOP_MAX ) // Range check requested frequency + freq = STOP_MAX; + + if ( freq < START_MIN ) // Range check + freq = START_MIN; + + +/* + * Compute new start and stop frequencies based on the requested frequenct and whether + * a "WIDE" span or a "NARROW" span was requested. + */ + + if ( span == WIDE ) // Using normal span? + halfSpan = freq / 2; // Then halfSpan is half the frequency + + else // Must be a "NARROW" span + halfSpan = freq / FOCUS_FACTOR / 2; // Span is banse on the "FOCUS_FACTOR" + + startFreq = freq - halfSpan; // New computed start frequency + stopFreq = freq + halfSpan; // New computed stop frequency + + +/* + * From the Department of Redundency Department: Now we have to range check the computed + * start and stop frequencies and if either of those is out of bounds, we need to adjust the + * computed span accordingly. We'll maintain the requested center frequency though. + */ + + if ( stopFreq > STOP_MAX ) // Range check the stop frequency + { + stopFreq = STOP_MAX; // Set the stop frequency at the upper limit + halfSpan = stopFreq - freq; // Re-compute half span + startFreq = freq - halfSpan; // and recompute start frequency + } + + if ( startFreq < START_MIN ) // Range check the start frequency + { + startFreq = START_MIN; // Set start at the limit + halfSpan = freq - startFreq; // Re-compute half span + stopFreq = freq + halfSpan; // and re-compute stop frequency + } + + startFreq = freq - halfSpan; // Re-compute start and + stopFreq = freq + halfSpan; // stop frequencies + +// Serial.print ( "Computed start = " ); Serial.println ( FormatFrequency ( startFreq )); +// Serial.print ( "Computed stop = " ); Serial.println ( FormatFrequency ( stopFreq )); +// Serial.print ( "Half span = " ); Serial.println ( FormatFrequency ( halfSpan )); + + setting.ScanStart = startFreq; // Set new start frequency + setting.ScanStop = stopFreq; // Set new stop frequency + + if (( setting.ScanStart != lastStart ) + || ( setting.ScanStop != lastStop )) // Did it change + { + changedSetting = true; // Yes it did + RedrawHisto (); // Redraw labels and restart sweep with new settings + WriteSettings (); // and save the setting structure + } +} + +uint32_t GetSweepCenter () +{ + return ( setting.ScanStart + setting.ScanStop ) / 2; +} + + +/* + * "SetSweepSpan" will maintain the previous center frequency and attempt to simply + * change the range of the scan. If the new limits cause the start frequency to go + * or the stop frequency to exceed "STOP_MAX", the range will be adjusted to maintain + * an equal range on both sides of the center frequency. + */ + +void SetSweepSpan ( uint32_t spanRange ) +{ +uint32_t startFreq; // These are used in computing the +uint32_t stopFreq; // Scan limits +uint32_t centerFreq; // Current center frequency +uint32_t halfSpan; // Half of the frequency span + +uint32_t lastStart; // Old ssweep start frequency +uint32_t lastStop; // and old stop frequency + +// Serial.print ( "Requested span = " ); Serial.println ( FormatFrequency ( spanRange )); + + lastStart = setting.ScanStart; // Save the current start frequency + lastStop = setting.ScanStop; // and the current stop frequency + + halfSpan = spanRange / 2; // Half the requested span + centerFreq = GetSweepCenter (); // Current center frequency + + startFreq = spanRange - halfSpan; // New computed start frequency + stopFreq = spanRange + halfSpan; // New computed stop frequency + + +/* + * Now we have to range check the computed start and stop frequencies and if either + * of those is out of bounds, we will adjust the span accordingly. We'll maintain + * the previous center frequency though. + */ + + if ( stopFreq > STOP_MAX ) // Range check the stop frequency + { + stopFreq = STOP_MAX; // Set the stop frequency at the upper limit + halfSpan = stopFreq - centerFreq; // Re-compute half span + startFreq = centerFreq - halfSpan; // and recompute start frequency + } + + if ( startFreq < START_MIN ) // Range check the start frequency + { + startFreq = START_MIN; // Set start at the limit + halfSpan = centerFreq - startFreq; // Re-compute half span + stopFreq = centerFreq + halfSpan; // and re-compute stop frequency + } + + startFreq = centerFreq - halfSpan; // Re-compute start and + stopFreq = centerFreq + halfSpan; // stop frequencies + +// Serial.print ( "Computed start = " ); Serial.println ( FormatFrequency ( startFreq )); +// Serial.print ( "Computed stop = " ); Serial.println ( FormatFrequency ( stopFreq )); +// Serial.print ( "Half span = " ); Serial.println ( FormatFrequency ( halfSpan )); + + setting.ScanStart = startFreq; // Set new start frequency + setting.ScanStop = stopFreq; // Set new stop frequency + + if (( setting.ScanStart != lastStart ) + || ( setting.ScanStop != lastStop )) // Did it change + { + changedSetting = true; // Yes it did + RedrawHisto (); // Redraw labels and restart sweep with new settings + WriteSettings (); // and save the setting structure + } +} + + +uint32_t GetSweepSpan () +{ + return setting.ScanStop - setting.ScanStart; +} + + +/* + * "SetFreq" is used by the serial command handler to set a specific frequency + * for one of the Si4432 modules. + */ + +void SetFreq ( int vfo, uint32_t freq ) +{ + if ( vfo == RX_4432 ) // Receiver? + rcvr.SetFrequency ( freq ); + + if ( vfo == TX_4432 ) // Transmitter? + xmit.SetFrequency ( freq ); +} + +void SetWFMin (int16_t level) +{ + if ( (level >=-130) && (level < -50) ) + setting.WaterfallMin = dBmToRSSI( (double)level ); + Serial.printf("Watterfall min set to %i \n", setting.WaterfallMin ); + WriteSettings(); +} + +void SetWFGain (float gain) +{ + if ((gain > 0.1) && (gain < 3.0)) + setting.WaterfallGain = gain; + WriteSettings(); +} diff --git a/cmd.h b/cmd.h new file mode 100644 index 0000000..526ab6a --- /dev/null +++ b/cmd.h @@ -0,0 +1,239 @@ +/* + * "Cmd.h" was added in Version 2.0 by John Price (WA2FZW) + * + * This file contains definitions and function prototypes associated with + * the "Cmd.cpp" module. + * + * In Version 2.0 of the software, all of the serial input command handling + * was removed from the main program sile into these modules. + */ + + +#ifndef _CMD_H_ +#define _CMD_H_ // Prevent double inclusion + +#include "simpleSA.h" // General program definitions +#include "pE4302.h" // Class definition for thePE4302 attenuator +#include "si4432.h" // Class definition for the Si4432 transceiver +#include "preferencesd.h" // For saving the "setting" structure +#include + + +/* + * These are numerical values which define the message being processed. We could just + * work off the ASCII string, but translating the strings to a numerical value makes + * the processing a bit easier and also keeps open the possibility of using more tha + * one ASCII string to implement a give command. + */ + +#define MSG_NONE 0 // Unrecognized command +#define MSG_START 1 // Set sweep start frequency +#define MSG_STOP 2 // Set sweep stop frequency +#define MSG_CENTER 3 // Set sweep center frequency +#define MSG_SPAN 4 // Set sweep range +#define MSG_FOCUS 5 // Center frequency with narrow sweep +#define MSG_DRIVE 6 // Set transmitter (LO) output level +#define MSG_VFO_FREQ 7 // Set the frequency for the selected VFO +#define MSG_ATTEN 8 // Set the PE4301 attenuation +#define MSG_HELP 9 // Display the command menu +#define MSG_STEPS 10 // Set or get number of sweep points (not used) +#define MSG_DELAY 11 // Set or get delay time between sweep readings +#define MSG_VFO 12 // Set or get the currently selected VFO +#define MSG_RBW 13 // Set or get the current resolution bandwidth +#define MSG_REG_DUMP 14 // Print register values for the selected VFO +#define MSG_RSSI 15 // Show RSSI values +#define MSG_QUIT 16 // Stop RSSI readings +#define MSG_SET_REG 17 // Set or get the value of a specifix register for the selected VFO +#define MSG_SAVE 18 // Save scan configuration +#define MSG_RECALL 19 // Recall saved scan configuration +#define MSG_GPIO2 20 // Set transmitter GPIO2 reference frequency +#define MSG_TUNE 21 // Tune selected Si4432 (VFO) +#define MSG_CONFIG 22 // Save "config" structure +#define MSG_ACT_PWR 23 // Calibrate the observed power level +#define MSG_IF_FREQ 24 // Set the IF frequency +#define MSG_TRACES 25 // Turn things on the display on or off +#define MSG_PREAMP 26 // Set the receiver preamp gain +#define MSG_GRID 27 // Set the dB value for top line of the grid +#define MSG_SCALE 28 // Set the dB/horizontal line for the grid +#define MSG_PAUSE 29 // Pause (or resume) the sweep +#define MSG_SWEEPLO 30 // Set Analyse low range mode +#define MSG_SIGLO 31 // Signal generate low range mode +#define MSG_IF_SWEEP 32 // Set IF Sweep mode +#define MSG_MARKER 33 // Configure Markers +#define MSG_SPUR 34 // Set Spur reduction on or off +#define MSG_WIFI_UPDATE 35 // Set WiFi update target time in us +#define MSG_WIFI_POINTS 36 // Set WiFi chunk size +#define MSG_WEBSKT_INTERVAL 37 // Set interval between checking websocket for events if no client connected +#define MSG_SG_RX_DRIVE 38 // Set Signal Generator RX (IF) Drive level - don't go above the 10dBm rating of the SAW filters +#define MSG_SG_LO_DRIVE 39 // Set Signal Generator LO Drive level - limited by mixer/attenuators fitted +#define MSG_TG_IF_DRIVE 40 // Set Tracking Generator RX (IF) Drive level - don't go above the 10dBm rating of the SAW filters +#define MSG_TG_LO_DRIVE 41 // Set Tracking Generator LO Drive level - limited by mixer/attenuators fitted +#define MSG_IFSIGNAL 42 // Set frequency of injected signal for IF Sweep +#define MSG_TGOFFSET 43 // Offset TG IF frequency from SA IF +#define MSG_BANDSCOPE 44 // Set Bandscope Mode +#define MSG_IFSWEEP 45 // Set IF Sweep Mode +#define MSG_OFFDELAY 46 // Adjust offset tuning delay time before taking reading +#define MSG_WFMIN 47 // set the min level for the waterfall colours +#define MSG_WFGAIN 48 // set the gain for waterfall colouring +#define MSG_RX_SWEEP 49 // Set the RX Sweep Mode +#define MSG_RXSIGNAL 50 // Set frequency of injected signal for IF Sweep +#define MSG_EXTGAIN 51 // Set external gain or attenuation +#define MSG_SGON 52 // Turn Signal Generator output on +#define MSG_SGOFF 53 // Turn Signal Generator output off +#define MSG_SGFREQ 54 // Set Signal Generator Frequency +#define MSG_TGON 55 // Turn Tracking Generator output on +#define MSG_TGOFF 56 // Turn Tracking Generator output off +#define MSG_TGFREQ 57 // Set Track Gen frequency for sig gen mode +#define MSG_TGSIG 58 // Set Track Gen sig gen mode +#define MSG_BPFCAL 59 // Start bandpass filter calibration +#define MSG_OTA 60 // OTA Update mode + +#define MSG_COLOURTEST 99 // test of waterfall colours - remove this when done! + + +typedef struct // Keeps everything together +{ + char Name[20]; // ASCII Command string + uint8_t ID; // The internal message number +} msg_t; + + +/* + * This "enum" is used by the "SetSweepCenter" function to designate whether we want a + * "WIDE" span when setting the normal center frequency, or a "NARROW" span when setting + * a focus frequency. + */ + +enum { WIDE, NARROW }; + + +/* + * Function prototypes for the help menu display and main command processing: + */ + +void ShowMenu (); // Displays the command menu + +bool CheckCommand (); // Checks for a new command +bool ParseCommand ( char* inBuff ); // Separate command and data +bool ProcessCommand ( uint8_t command, char* dataBuff ); + + +/* + * Function prototypes for general support functions: + */ + +uint32_t ParseFrequency ( char* freqString ); // Handles various frequency input formats +int32_t ParseSignedFrequency ( char* freqString ); // Handles various frequency input formats +char* FormatFrequency ( uint32_t freq ); // Neatly formats frequencies for output +char* FormatSignedFrequency ( int32_t freq ); // Neatly formats signed frequencies for output +char* DisplayFrequency ( uint32_t freq ); // Neatly formats frequencies for sig gen display + +uint16_t xtoi ( char* hexString ); // Converts hexadecimal strings into integers +bool isHex ( char c ); // Tests if a character is a hexadecimal digit + + +/* + * These functions all support the main command processing functions and are used to + * set the values of things either in the appropriate internal variables and/or in + * the PE4302 or Si4432 modules themselves. + */ + +void SetRefOutput ( int freq ); // Sets the GPIO2 frequency for the LO + +void SetRefLevel ( int ref ); // Sets the decibel level for the top line of the graph +void SetPowerGrid ( int g ); // Sets the dB/vertical divison on the grid + +void SetGenerate ( int8_t g ); // Puts the unit into or out of signal generator mode + +bool SetIFFrequency ( int32_t f ); // Sets the IF frequency + +void SetLoDrive ( uint8_t level ); // Sets LO Si4432 output level +void SetSGState (uint16_t s); +bool SetSGFreq ( uint32_t freq); // set signal generator frequency - returns false if invalid +void SetSGLoDrive ( uint8_t level ); +void SetSGRxDrive ( uint8_t level ); +void SetSGPower ( int16_t dBm ); // Set signal generator attenuator and drive levels + +void SetTracking ( int8_t m ); // set tracking generator mode +void SetTGLoDrive ( uint8_t level ); // set tracking generator drive +void SetTGIfDrive ( uint8_t level ); +bool SetTGOffset ( int32_t offset); // set tracking generator offset - returns false if invalid +bool SetTGFreq ( int32_t freq); // set tracking generator freq for sig gen mode - returns false if invalid +int32_t GetTGOffset (void ); +void SetTGPower ( int16_t dBm ); // Set TG attenuator and drive levels + +void SetAttenuation ( int a ); // Sets the PE4302 attenuation + +void SetExtGain ( double a ); // Sets the external gain or attenuation +double GetExtGain (void ); + +void SetStorage ( void ); // Saves the results of a scan +void SetClearStorage ( void ); // Logically erases the saved scan +void SetSubtractStorage(void); // Sets the "setting.SubtractStorage" flag + +void RequestSetPowerLevel ( float o ); // Power level calibration +void SetPowerLevel ( double o ); // ??? + +void SetRBW ( int v ); // Sets the resolution bandwidth + +void SetSpur (int v ); // Turns spurious signal supression on or off + +void SetAverage ( int v ); // Sets the number of readings to average + +void SetPreampGain ( uint8_t gain ); // Set and get the receiver preamp gain +uint8_t GetPreampGain ( bool* agc, uint8_t* reg ); + +void SetSweepStart ( uint32_t freq ); // Added in Version 2.3 +uint32_t GetSweepStart ( void ); + +void SetSweepStop ( uint32_t freq ); // Added in Version 2.3 +uint32_t GetSweepStop ( void ); + +void SetBandscopeStart ( uint32_t freq ); // Added in Version 3.0f +uint32_t GetBandscopeStart ( void ); + +void SetBandscopeSpan ( uint32_t freq ); // Added in Version 3.0f +uint32_t GetBandscopeSpan ( void ); + +void SetBandscopeRBW ( int v ); // Sets the resolution bandwidth + +void SetBandscopeLevel ( int ref ); // Sets the decibel level for the top line of the graph + +void SetIFsweepStart ( uint32_t freq ); // Added in Version 3.0c +uint32_t GetIFsweepStart ( void ); + +void SetIFsweepStop ( uint32_t freq ); // Added in Version 3.0c +uint32_t GetIFsweepStop ( void ); + +void SetIFsweepSigFreq ( uint32_t freq ); // Added in Version 3.0c +uint32_t GetIFsweepSigFreq ( void ); + +void SetRXsweepStart ( uint32_t freq ); +uint32_t GetRXsweepStart ( void ); + +void SetRXsweepStop ( uint32_t freq ); +uint32_t GetRXsweepStop ( void ); + +void SetRXsweepSpan ( uint32_t freq ); +uint32_t GetRXsweepSpan ( void ); + +void SetRXsweepSigFreq ( uint32_t freq ); +uint32_t GetRXsweepSigFreq ( void ); + +boolean StartBpfCal ( void ); + +void SetSweepCenter ( uint32_t freq, uint8_t span ); +uint32_t GetSweepCenter ( void ); + +void SetSweepSpan ( uint32_t spanRange ); +uint32_t GetSweepSpan ( void ); + +void SetFreq ( int vfo, uint32_t freq ); + +bool UpdateMarker ( uint8_t mkr, char action ); + +void SetWFMin (int16_t level); +void SetWFGain (float gain); + + +#endif // End of "Cmd.h" diff --git a/data/NotoSansBold56.vlw b/data/NotoSansBold56.vlw new file mode 100644 index 0000000000000000000000000000000000000000..14c889ff0d1e786d19c896032777ca0ea31e3512 GIT binary patch literal 100463 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 z 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 V`ul%z^B+cki^ClJI~@GY{}1K{lnMX< literal 0 HcmV?d00001 diff --git a/data/NotoSansSCM14.vlw b/data/NotoSansSCM14.vlw new file mode 100644 index 0000000000000000000000000000000000000000..0ce9369c15361fc0756ea0b5303a71e61994e1ae GIT binary patch literal 26168 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 u-ProQOW*$dyWjcN7pu>=@d?CjEcif4wRI literal 0 HcmV?d00001 diff --git a/data/about.html b/data/about.html new file mode 100644 index 0000000..dc41423 --- /dev/null +++ b/data/about.html @@ -0,0 +1,63 @@ + + + + [simpleSA] + + + + + + + + + + + +
+ +

+ [programName] [programVersion] +

+ + + +
+
+ + + +
+ +
+ +

About

+ +

+ This simpleSA running on an ESP32 has been adapted by Dave M0WID and John WA2FZW.
+ It was based on an early arduino prototype of the TinySA by Erik PD0EK.
+ Information can be found at https://groups.io/g/HBTE +

+ +
+
+ To reboot click +
+
+
+ + + diff --git a/data/backupRestore.html b/data/backupRestore.html new file mode 100644 index 0000000..fdeb2d5 --- /dev/null +++ b/data/backupRestore.html @@ -0,0 +1,215 @@ + + + + + [RBNSpyBox] + + + + + + + + + + + +
+ +

+ [programName] [programVersion] +

+ + + + + + + +
+
+ + + +
+ +
+ +

Backup

+

+

+ To backup your friends click and save the resulting + download safely. +
+

+
+

Restore

+

+ To restore your friends drag and drop your data file below. +

+
+ Here... +
+ +

+ +

+
+
+ + + diff --git a/data/canvasjs.min.js b/data/canvasjs.min.js new file mode 100644 index 0000000..c9cbd78 --- /dev/null +++ b/data/canvasjs.min.js @@ -0,0 +1,911 @@ +/* + CanvasJS HTML5 & JavaScript Charts - v2.3.2 GA - https://canvasjs.com/ + Copyright 2018 fenopix + + --------------------- License Information -------------------- + CanvasJS is a commercial product which requires purchase of license. Without a commercial license you can use it for evaluation purposes for upto 30 days. Please refer to the following link for further details. + https://canvasjs.com/license/ + +*/ +/*eslint-disable*/ +/*jshint ignore:start*/ +(function(){function qa(h,p){h.prototype=eb(p.prototype);h.prototype.constructor=h;h.base=p.prototype}function eb(h){function p(){}p.prototype=h;return new p}function Ya(h,p,D){"millisecond"===D?h.setMilliseconds(h.getMilliseconds()+1*p):"second"===D?h.setSeconds(h.getSeconds()+1*p):"minute"===D?h.setMinutes(h.getMinutes()+1*p):"hour"===D?h.setHours(h.getHours()+1*p):"day"===D?h.setDate(h.getDate()+1*p):"week"===D?h.setDate(h.getDate()+7*p):"month"===D?h.setMonth(h.getMonth()+1*p):"year"===D&&h.setFullYear(h.getFullYear()+ +1*p);return h}function $(h,p){var D=!1;0>h&&(D=!0,h*=-1);h=""+h;for(p=p?p:1;h.length
Please right click on the image and save it to your device
"), +p.document.close()}}}function N(h){var p=((h&16711680)>>16).toString(16),D=((h&65280)>>8).toString(16);h=((h&255)>>0).toString(16);p=2>p.length?"0"+p:p;D=2>D.length?"0"+D:D;h=2>h.length?"0"+h:h;return"#"+p+D+h}function fb(h,p){var D=this.length>>>0,r=Number(p)||0,r=0>r?Math.ceil(r):Math.floor(r);for(0>r&&(r+=D);rD;D++)if(h[D]!==h[D+4]|h[D]!==h[D+8]|h[D]!==h[D+12]){p=!1;break}return p?h[0]<<16|h[1]<<8|h[2]:0}function na(h,p,D){return h in p?p[h]:D[h]}function Oa(h,p,D){if(r&&bb){var u=h.getContext("2d");Pa=u.webkitBackingStorePixelRatio|| +u.mozBackingStorePixelRatio||u.msBackingStorePixelRatio||u.oBackingStorePixelRatio||u.backingStorePixelRatio||1;W=Ua/Pa;h.width=p*W;h.height=D*W;Ua!==Pa&&(h.style.width=p+"px",h.style.height=D+"px",u.scale(W,W))}else h.width=p,h.height=D}function hb(h){if(!ib){var p=!1,D=!1;"undefined"===typeof ra.Chart.creditHref?(h.creditHref=ja("iuuqr;..b`ow`rkr/bnl."),h.creditText=ja("B`ow`rKR/bnl")):(p=h.updateOption("creditText"),D=h.updateOption("creditHref"));if(h.creditHref&&h.creditText){h._creditLink|| +(h._creditLink=document.createElement("a"),h._creditLink.setAttribute("class","canvasjs-chart-credit"),h._creditLink.setAttribute("title","JavaScript Charts"),h._creditLink.setAttribute("style","outline:none;margin:0px;position:absolute;right:2px;top:"+(h.height-14)+"px;color:dimgrey;text-decoration:none;font-size:11px;font-family: Calibri, Lucida Grande, Lucida Sans Unicode, Arial, sans-serif"),h._creditLink.setAttribute("tabIndex",-1),h._creditLink.setAttribute("target","_blank"));if(0===h.renderCount|| +p||D)h._creditLink.setAttribute("href",h.creditHref),h._creditLink.innerHTML=h.creditText;h._creditLink&&h.creditHref&&h.creditText?(h._creditLink.parentElement||h._canvasJSContainer.appendChild(h._creditLink),h._creditLink.style.top=h.height-14+"px"):h._creditLink.parentElement&&h._canvasJSContainer.removeChild(h._creditLink)}}}function ta(h,p){Ja&&(this.canvasCount|=0,window.console.log(++this.canvasCount));var D=document.createElement("canvas");D.setAttribute("class","canvasjs-chart-canvas");Oa(D, +h,p);r||"undefined"===typeof G_vmlCanvasManager||G_vmlCanvasManager.initElement(D);return D}function sa(h,p,D){for(var r in D)p.style[r]=D[r]}function ua(h,p,D){p.getAttribute("state")||(p.style.backgroundColor=h.toolbar.backgroundColor,p.style.color=h.toolbar.fontColor,p.style.border="none",sa(h,p,{WebkitUserSelect:"none",MozUserSelect:"none",msUserSelect:"none",userSelect:"none"}));p.getAttribute("state")!==D&&(p.setAttribute("state",D),p.setAttribute("type","button"),sa(h,p,{padding:"5px 12px", +cursor:"pointer","float":"left",width:"40px",height:"25px",outline:"0px",verticalAlign:"baseline",lineHeight:"0"}),p.setAttribute("title",h._cultureInfo[D+"Text"]),p.innerHTML=""+h._cultureInfo[D+"Text"]+"")}function Qa(){for(var h=null,p=0;pa?"a":"p";case "tt":return 12>a?"am":"pm";case "T":return 12>a?"A": +"P";case "TT":return 12>a?"AM":"PM";case "K":return S?"UTC":(String(z).match(H)||[""]).pop().replace(F,"");case "z":return(0h?!0:!1;u&&(h*=-1);var v=r?r.decimalSeparator:".",H=r?r.digitGroupSeparator: +",",F="";p=String(p);var F=1,z=r="",E=-1,L=[],R=[],I=0,N=0,S=0,O=!1,U=0,z=p.match(/"[^"]*"|'[^']*'|[eE][+-]*[0]+|[,]+[.]|\u2030|./g);p=null;for(var Q=0;z&&QE)E=Q;else{if("%"===p)F*=100;else if("\u2030"===p){F*=1E3;continue}else if(","===p[0]&&"."===p[p.length-1]){F/=Math.pow(1E3,p.length-1);E=Q+p.length-1;continue}else"E"!==p[0]&&"e"!==p[0]||"0"!==p[p.length-1]||(O=!0);0>E?(L.push(p),"#"===p||"0"===p?I++:","===p&&S++):(R.push(p),"#"!==p&&"0"!==p||N++)}O&&(p=Math.floor(h), +z=-Math.floor(Math.log(h)/Math.LN10+1),U=0===h?0:0===p?-(I+z):String(p).length-I,F/=Math.pow(10,U));0>E&&(E=Q);F=(h*F).toFixed(N);p=F.split(".");F=(p[0]+"").split("");h=(p[1]+"").split("");F&&"0"===F[0]&&F.shift();for(O=z=Q=N=E=0;0U?p.replace("+","").replace("-",""):p.replace("-",""),r+=p.replace(/[0]+/,function(h){return $(U,h.length)}));H="";for(L=!1;0U?p.replace("+","").replace("-",""):p.replace("-",""),H+=p.replace(/[0]+/,function(h){return $(U,h.length)}));r+=(L?v:"")+H;return u?"-"+r:r},Ra=function(h){var p=0,r=0;h=h||window.event;h.offsetX||0===h.offsetX?(p=h.offsetX,r=h.offsetY):h.layerX||0==h.layerX?(p=h.layerX,r=h.layerY):(p=h.pageX-h.target.offsetLeft, +r=h.pageY-h.target.offsetTop);return{x:p,y:r}},bb=!0,Ua=window.devicePixelRatio||1,Pa=1,W=bb?Ua/Pa:1,ea=function(h,p,r,u,v,H,F,z,E,L,R,N,O){"undefined"===typeof O&&(O=1);F=F||0;z=z||"black";var I=15p)v=H-1;else break}r>p&&1H&&(F=p.pop(),u-=F.height,v=z)}this._wrappedText={lines:p,width:v,height:u};this.width=v+(this.leftPadding+this.rightPadding);this.height=u+(this.topPadding+this.bottomPadding);this.ctx.font=r};ka.prototype._getFontString=function(){var h;h=""+(this.fontStyle?this.fontStyle+" ":"");h+=this.fontWeight?this.fontWeight+" ":"";h+=this.fontSize?this.fontSize+"px ":"";var p=this.fontFamily?this.fontFamily+"":"";!r&&p&&(p=p.split(",")[0],"'"!==p[0]&&'"'!==p[0]&&(p="'"+p+"'"));return h+=p}; +qa(Va,V);qa(Aa,V);Aa.prototype.setLayout=function(){if(this.text){var h=this.dockInsidePlotArea?this.chart.plotArea:this.chart,p=h.layoutManager.getFreeSpace(),r=p.x1,v=p.y1,E=0,H=0,F=this.chart._menuButton&&this.chart.exportEnabled&&"top"===this.verticalAlign?22:0,z,I;"top"===this.verticalAlign||"bottom"===this.verticalAlign?(null===this.maxWidth&&(this.maxWidth=p.width-4-F*("center"===this.horizontalAlign?2:1)),H=0.5*p.height-this.margin-2,E=0):"center"===this.verticalAlign&&("left"===this.horizontalAlign|| +"right"===this.horizontalAlign?(null===this.maxWidth&&(this.maxWidth=p.height-4),H=0.5*p.width-this.margin-2):"center"===this.horizontalAlign&&(null===this.maxWidth&&(this.maxWidth=p.width-4),H=0.5*p.height-4));var L;u(this.padding)||"number"!==typeof this.padding?u(this.padding)||"object"!==typeof this.padding||(L=this.padding.top?this.padding.top:this.padding.bottom?this.padding.bottom:0,L+=this.padding.bottom?this.padding.bottom:this.padding.top?this.padding.top:0,L*=1.25):L=2.5*this.padding;this.wrap|| +(H=Math.min(H,Math.max(1.5*this.fontSize,this.fontSize+L)));H=new ka(this.ctx,{fontSize:this.fontSize,fontFamily:this.fontFamily,fontColor:this.fontColor,fontStyle:this.fontStyle,fontWeight:this.fontWeight,horizontalAlign:this.horizontalAlign,verticalAlign:this.verticalAlign,borderColor:this.borderColor,borderThickness:this.borderThickness,backgroundColor:this.backgroundColor,maxWidth:this.maxWidth,maxHeight:H,cornerRadius:this.cornerRadius,text:this.text,padding:this.padding,textBaseline:"top"}); +L=H.measureText();"top"===this.verticalAlign||"bottom"===this.verticalAlign?("top"===this.verticalAlign?(v=p.y1+2,I="top"):"bottom"===this.verticalAlign&&(v=p.y2-2-L.height,I="bottom"),"left"===this.horizontalAlign?r=p.x1+2:"center"===this.horizontalAlign?r=p.x1+p.width/2-L.width/2:"right"===this.horizontalAlign&&(r=p.x2-2-L.width-F),z=this.horizontalAlign,this.width=L.width,this.height=L.height):"center"===this.verticalAlign&&("left"===this.horizontalAlign?(r=p.x1+2,v=p.y2-2-(this.maxWidth/2-L.width/ +2),E=-90,I="left",this.width=L.height,this.height=L.width):"right"===this.horizontalAlign?(r=p.x2-2,v=p.y1+2+(this.maxWidth/2-L.width/2),E=90,I="right",this.width=L.height,this.height=L.width):"center"===this.horizontalAlign&&(v=h.y1+(h.height/2-L.height/2),r=h.x1+(h.width/2-L.width/2),I="center",this.width=L.width,this.height=L.height),z="center");H.x=r;H.y=v;H.angle=E;H.horizontalAlign=z;this._textBlock=H;h.layoutManager.registerSpace(I,{width:this.width+("left"===I||"right"===I?this.margin+2:0), +height:this.height+("top"===I||"bottom"===I?this.margin+2:0)});this.bounds={x1:r,y1:v,x2:r+this.width,y2:v+this.height};this.ctx.textBaseline="top"}};Aa.prototype.render=function(){this._textBlock&&this._textBlock.render(!0)};qa(Ka,V);Ka.prototype.setLayout=Aa.prototype.setLayout;Ka.prototype.render=Aa.prototype.render;Wa.prototype.get=function(h,p){var r=null;0a[g].x&&0w?{x:a[l].x+w/3,y:a[l].y+c/3}:{x:a[l].x,y:a[l].y+c/9};l=e;g=0===l?0:l-1;k=l===a.length-1?l:l+1;c=Math.abs((a[k].x-a[g].x)/(0===a[l].x-a[g].x?0.01:a[l].x-a[g].x))*(d- +1)/2+1;w=(a[k].x-a[g].x)/c;c=(a[k].y-a[g].y)/c;b[b.length]=a[l].x>a[g].x&&0w?{x:a[l].x-w/3,y:a[l].y-c/3}:{x:a[l].x,y:a[l].y-c/9};b[b.length]=a[e]}return b}function E(a,d,b,c,e,g,k,l,w,m){var s=0;m?(k.color=g,l.color=g):m=1;s=w?Math.abs(e-b):Math.abs(c-d);s=0this.labelAngle?this.labelAngle-=180:270<=this.labelAngle&&360>=this.labelAngle&&(this.labelAngle-=360);this.options.scaleBreaks&&(this.scaleBreaks=new Q(this.chart, +this.options.scaleBreaks,++this.chart._eventManager.lastObjectId,this));this.stripLines=[];if(this.options.stripLines&&0=this._appliedBreaks[a+1].startValue&&(this._appliedBreaks[a].endValue=Math.max(this._appliedBreaks[a].endValue,this._appliedBreaks[a+1].endValue),window.console&&window.console.log("CanvasJS Error: Breaks "+a+" and "+(a+1)+" are overlapping."),this._appliedBreaks.splice(a,2),a--)}}function L(a,d,b,c,e,g){L.base.constructor.call(this,"Break",d,b,c,g);this.id=e;this.chart=a;this.ctx=this.chart.ctx;this.scaleBreaks=g;this.optionsName= +d;this.isOptionsInArray=!0;this.type=b.type?this.type:g.type;this.fillOpacity=u(b.fillOpacity)?g.fillOpacity:this.fillOpacity;this.lineThickness=u(b.lineThickness)?g.lineThickness:this.lineThickness;this.color=b.color?this.color:g.color;this.lineColor=b.lineColor?this.lineColor:g.lineColor;this.lineDashType=b.lineDashType?this.lineDashType:g.lineDashType;!u(this.startValue)&&this.startValue.getTime&&(this.startValue=this.startValue.getTime());!u(this.endValue)&&this.endValue.getTime&&(this.endValue= +this.endValue.getTime());"number"===typeof this.startValue&&("number"===typeof this.endValue&&this.endValue=navigator.userAgent.search("MSIE")&&sa(a,a._zoomButton.childNodes[0],{WebkitFilter:"invert(100%)",filter:"invert(100%)"}))},this.allDOMEventHandlers);O(this._zoomButton,"mouseout",function(){d||(sa(a,a._zoomButton,{backgroundColor:a.toolbar.backgroundColor,color:a.toolbar.fontColor,transition:"0.4s",WebkitTransition:"0.4s"}),0>=navigator.userAgent.search("MSIE")&&sa(a,a._zoomButton.childNodes[0],{WebkitFilter:"invert(0%)", +filter:"invert(0%)"}))},this.allDOMEventHandlers)}this._resetButton||(d=!1,va(this._resetButton=document.createElement("button")),ua(this,this._resetButton,"reset"),this._resetButton.style.borderRight=(this.exportEnabled?this.toolbar.borderThickness:0)+"px solid "+this.toolbar.borderColor,this._toolBar.appendChild(this._resetButton),O(this._resetButton,"touchstart",function(a){d=!0},this.allDOMEventHandlers),O(this._resetButton,"click",function(){a.toolTip.hide();a.zoomEnabled||a.panEnabled?(a.zoomEnabled= +!0,a.panEnabled=!1,ua(a,a._zoomButton,"pan"),a._defaultCursor="default",a.overlaidCanvas.style.cursor=a._defaultCursor):(a.zoomEnabled=!1,a.panEnabled=!1);if(a.sessionVariables.axisX)for(var c=0;c=navigator.userAgent.search("MSIE")&&sa(a,a._resetButton.childNodes[0],{WebkitFilter:"invert(100%)",filter:"invert(100%)"}))},this.allDOMEventHandlers),O(this._resetButton,"mouseout",function(){d||(sa(a,a._resetButton, +{backgroundColor:a.toolbar.backgroundColor,color:a.toolbar.fontColor,transition:"0.4s",WebkitTransition:"0.4s"}),0>=navigator.userAgent.search("MSIE")&&sa(a,a._resetButton.childNodes[0],{WebkitFilter:"invert(0%)",filter:"invert(0%)"}))},this.allDOMEventHandlers),this.overlaidCanvas.style.cursor=a._defaultCursor);this.zoomEnabled||this.panEnabled||(this._zoomButton?(a._zoomButton.getAttribute("state")===a._cultureInfo.zoomText?(this.panEnabled=!0,this.zoomEnabled=!1):(this.zoomEnabled=!0,this.panEnabled= +!1),Qa(a._zoomButton,a._resetButton)):(this.zoomEnabled=!0,this.panEnabled=!1))}else this.panEnabled=this.zoomEnabled=!1;this._menuButton?this.exportEnabled?Qa(this._menuButton):va(this._menuButton):this.exportEnabled&&r&&(d=!1,this._menuButton=document.createElement("button"),ua(this,this._menuButton,"menu"),this._toolBar.appendChild(this._menuButton),O(this._menuButton,"touchstart",function(a){d=!0},this.allDOMEventHandlers),O(this._menuButton,"click",function(){"none"!==a._dropdownMenu.style.display|| +a._dropDownCloseTime&&500>=(new Date).getTime()-a._dropDownCloseTime.getTime()||(a._dropdownMenu.style.display="block",a._menuButton.blur(),a._dropdownMenu.focus())},this.allDOMEventHandlers,!0),O(this._menuButton,"mouseover",function(){d||(sa(a,a._menuButton,{backgroundColor:a.toolbar.backgroundColorOnHover,color:a.toolbar.fontColorOnHover}),0>=navigator.userAgent.search("MSIE")&&sa(a,a._menuButton.childNodes[0],{WebkitFilter:"invert(100%)",filter:"invert(100%)"}))},this.allDOMEventHandlers,!0), +O(this._menuButton,"mouseout",function(){d||(sa(a,a._menuButton,{backgroundColor:a.toolbar.backgroundColor,color:a.toolbar.fontColor}),0>=navigator.userAgent.search("MSIE")&&sa(a,a._menuButton.childNodes[0],{WebkitFilter:"invert(0%)",filter:"invert(0%)"}))},this.allDOMEventHandlers,!0));if(!this._dropdownMenu&&this.exportEnabled&&r){d=!1;this._dropdownMenu=document.createElement("div");this._dropdownMenu.setAttribute("tabindex",-1);var b=-1!==this.theme.indexOf("dark")?"black":"#888888";this._dropdownMenu.style.cssText= +"position: absolute; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; cursor: pointer;right: 0px;top: 25px;min-width: 120px;outline: 0;font-size: 14px; font-family: Arial, Helvetica, sans-serif;padding: 5px 0px 5px 0px;text-align: left;line-height: 10px;background-color:"+this.toolbar.backgroundColor+";box-shadow: 2px 2px 10px "+b;a._dropdownMenu.style.display="none";this._toolBar.appendChild(this._dropdownMenu);O(this._dropdownMenu,"blur",function(){va(a._dropdownMenu); +a._dropDownCloseTime=new Date},this.allDOMEventHandlers,!0);b=document.createElement("div");b.style.cssText="padding: 12px 8px 12px 8px";b.innerHTML=this._cultureInfo.printText;b.style.backgroundColor=this.toolbar.backgroundColor;b.style.color=this.toolbar.fontColor;this._dropdownMenu.appendChild(b);O(b,"touchstart",function(a){d=!0},this.allDOMEventHandlers);O(b,"mouseover",function(){d||(this.style.backgroundColor=a.toolbar.backgroundColorOnHover,this.style.color=a.toolbar.fontColorOnHover)},this.allDOMEventHandlers, +!0);O(b,"mouseout",function(){d||(this.style.backgroundColor=a.toolbar.backgroundColor,this.style.color=a.toolbar.fontColor)},this.allDOMEventHandlers,!0);O(b,"click",function(){a.print();va(a._dropdownMenu)},this.allDOMEventHandlers,!0);b=document.createElement("div");b.style.cssText="padding: 12px 8px 12px 8px";b.innerHTML=this._cultureInfo.saveJPGText;b.style.backgroundColor=this.toolbar.backgroundColor;b.style.color=this.toolbar.fontColor;this._dropdownMenu.appendChild(b);O(b,"touchstart",function(a){d= +!0},this.allDOMEventHandlers);O(b,"mouseover",function(){d||(this.style.backgroundColor=a.toolbar.backgroundColorOnHover,this.style.color=a.toolbar.fontColorOnHover)},this.allDOMEventHandlers,!0);O(b,"mouseout",function(){d||(this.style.backgroundColor=a.toolbar.backgroundColor,this.style.color=a.toolbar.fontColor)},this.allDOMEventHandlers,!0);O(b,"click",function(){Ta(a.canvas,"jpeg",a.exportFileName);va(a._dropdownMenu)},this.allDOMEventHandlers,!0);b=document.createElement("div");b.style.cssText= +"padding: 12px 8px 12px 8px";b.innerHTML=this._cultureInfo.savePNGText;b.style.backgroundColor=this.toolbar.backgroundColor;b.style.color=this.toolbar.fontColor;this._dropdownMenu.appendChild(b);O(b,"touchstart",function(a){d=!0},this.allDOMEventHandlers);O(b,"mouseover",function(){d||(this.style.backgroundColor=a.toolbar.backgroundColorOnHover,this.style.color=a.toolbar.fontColorOnHover)},this.allDOMEventHandlers,!0);O(b,"mouseout",function(){d||(this.style.backgroundColor=a.toolbar.backgroundColor, +this.style.color=a.toolbar.fontColor)},this.allDOMEventHandlers,!0);O(b,"click",function(){Ta(a.canvas,"png",a.exportFileName);va(a._dropdownMenu)},this.allDOMEventHandlers,!0)}"none"!==this._toolBar.style.display&&this._zoomButton&&(this.panEnabled?ua(a,a._zoomButton,"zoom"):ua(a,a._zoomButton,"pan"),a._resetButton.getAttribute("state")!==a._cultureInfo.resetText&&ua(a,a._resetButton,"reset"));this.options.toolTip&&this.toolTip.options!==this.options.toolTip&&(this.toolTip.options=this.options.toolTip); +for(var c in this.toolTip.options)this.toolTip.options.hasOwnProperty(c)&&this.toolTip.updateOption(c)};p.prototype._updateSize=function(){var a;a=[this.canvas,this.overlaidCanvas,this._eventManager.ghostCanvas];var d=0,b=0;this.options.width?d=this.width:this.width=d=0c.linkedDataSeriesIndex||c.linkedDataSeriesIndex>=this.options.data.length||"number"!==typeof c.linkedDataSeriesIndex||"error"===this.options.data[c.linkedDataSeriesIndex].type)&& +(c.linkedDataSeriesIndex=null);null===c.name&&(c.name="DataSeries "+a);null===c.color?1a&&"undefined"!==typeof w.startTimePercent?a>=w.startTimePercent&&w.animationCallback(w.easingFunction(a-w.startTimePercent,0,1,1-w.startTimePercent),w):w.animationCallback(w.easingFunction(a,0,1,1),w); +s.dispatchEvent("dataAnimationIterationEnd",{chart:s})},function(){b=[];for(var a=0;aa.dataSeriesIndexes.length))for(var d=a.axisY.dataInfo,b=a.axisX.dataInfo,c,e,g=!1,k=0;kb.max&&(b.max=c);ed.max&&"number"===typeof e&&(d.max=e);if(0B&&(B=1/B);b.minDiff>B&&1!==B&&(b.minDiff=B)}else B=c-l.dataPoints[w-1].x,0>B&& +(B*=-1),b.minDiff>B&&0!==B&&(b.minDiff=B);null!==e&&null!==l.dataPoints[w-1].y&&(a.axisY.logarithmic?(B=e/l.dataPoints[w-1].y,1>B&&(B=1/B),d.minDiff>B&&1!==B&&(d.minDiff=B)):(B=e-l.dataPoints[w-1].y,0>B&&(B*=-1),d.minDiff>B&&0!==B&&(d.minDiff=B)))}if(cf&&!s)s=!0;else if(c>f&&s)continue;l.dataPoints[w].label&&(a.axisX.labels[c]=l.dataPoints[w].label);cb.viewPortMax&&(b.viewPortMax=c);null===e?b.viewPortMin=== +c&&qd.viewPortMax&&"number"===typeof e&&(d.viewPortMax=e))}}l.axisX.valueType=l.xValueType=g?"dateTime":"number"}};p.prototype._processStackedPlotUnit=function(a){if(a.dataSeriesIndexes&&!(1>a.dataSeriesIndexes.length)){for(var d=a.axisY.dataInfo,b=a.axisX.dataInfo,c,e,g=!1,k=[],l=[],w=Infinity,m=-Infinity,s=0;sb.max&&(b.max=c);if(0r&&(r=1/r);b.minDiff>r&&1!==r&&(b.minDiff= +r)}else r=c-q.dataPoints[n-1].x,0>r&&(r*=-1),b.minDiff>r&&0!==r&&(b.minDiff=r);null!==e&&null!==q.dataPoints[n-1].y&&(a.axisY.logarithmic?0r&&(r=1/r),d.minDiff>r&&1!==r&&(d.minDiff=r)):(r=e-q.dataPoints[n-1].y,0>r&&(r*=-1),d.minDiff>r&&0!==r&&(d.minDiff=r)))}if(ct&&!B)B=!0;else if(c>t&&B)continue;q.dataPoints[n].label&&(a.axisX.labels[c]=q.dataPoints[n].label);cb.viewPortMax&&(b.viewPortMax=c);null===q.dataPoints[n].y?b.viewPortMin===c&&hd.max&&(d.max=a),nb.viewPortMax||(ad.viewPortMax&&(d.viewPortMax=a)));for(n in l)l.hasOwnProperty(n)&&!isNaN(n)&&(a=l[n],ad.max&&(d.max=Math.max(a,m)),nb.viewPortMax||(ad.viewPortMax&&(d.viewPortMax= +Math.max(a,m))))}};p.prototype._processStacked100PlotUnit=function(a){if(a.dataSeriesIndexes&&!(1>a.dataSeriesIndexes.length)){for(var d=a.axisY.dataInfo,b=a.axisX.dataInfo,c,e,g=!1,k=!1,l=!1,w=[],m=0;mb.max&&(b.max=c);if(0t&&(t=1/t);b.minDiff>t&&1!==t&&(b.minDiff=t)}else t=c-s.dataPoints[q-1].x,0>t&&(t*=-1),b.minDiff>t&&0!==t&&(b.minDiff=t);u(e)||null===s.dataPoints[q-1].y||(a.axisY.logarithmic?0t&&(t=1/t),d.minDiff>t&&1!==t&&(d.minDiff=t)):(t=e-s.dataPoints[q-1].y,0>t&&(t*=-1),d.minDiff>t&&0!==t&&(d.minDiff=t)))}if(cr&&!f)f=!0;else if(c>r&&f)continue;s.dataPoints[q].label&&(a.axisX.labels[c]=s.dataPoints[q].label);cb.viewPortMax&&(b.viewPortMax=c);null===e?b.viewPortMin===c&&Be&&(l=!0),w[c]=w[c]?w[c]+ +Math.abs(e):Math.abs(e))}}s.axisX.valueType=s.xValueType=g?"dateTime":"number"}a.axisY.logarithmic?(d.max=u(d.viewPortMax)?99*Math.pow(a.axisY.logarithmBase,-0.05):Math.max(d.viewPortMax,99*Math.pow(a.axisY.logarithmBase,-0.05)),d.min=u(d.viewPortMin)?1:Math.min(d.viewPortMin,1)):k&&!l?(d.max=u(d.viewPortMax)?99:Math.max(d.viewPortMax,99),d.min=u(d.viewPortMin)?1:Math.min(d.viewPortMin,1)):k&&l?(d.max=u(d.viewPortMax)?99:Math.max(d.viewPortMax,99),d.min=u(d.viewPortMin)?-99:Math.min(d.viewPortMin, +-99)):!k&&l&&(d.max=u(d.viewPortMax)?-1:Math.max(d.viewPortMax,-1),d.min=u(d.viewPortMin)?-99:Math.min(d.viewPortMin,-99));d.viewPortMin=d.min;d.viewPortMax=d.max;a.dataPointYSums=w}};p.prototype._processMultiYPlotUnit=function(a){if(a.dataSeriesIndexes&&!(1>a.dataSeriesIndexes.length))for(var d=a.axisY.dataInfo,b=a.axisX.dataInfo,c,e,g,k,l=!1,w=0;wb.max&& +(b.max=c);gd.max&&(d.max=k);0B&&(B=1/B),b.minDiff>B&&1!==B&&(b.minDiff=B)):(B=c-m.dataPoints[s-1].x,0>B&&(B*=-1),b.minDiff>B&&0!==B&&(b.minDiff=B)),e&&(null!==e[0]&&m.dataPoints[s-1].y&&null!==m.dataPoints[s-1].y[0])&&(a.axisY.logarithmic?(B=e[0]/m.dataPoints[s-1].y[0],1>B&&(B=1/B),d.minDiff>B&&1!==B&&(d.minDiff=B)):(B=e[0]-m.dataPoints[s-1].y[0],0>B&&(B*=-1),d.minDiff>B&&0!==B&&(d.minDiff=B))));if(!(ct&&!n)n=!0;else if(c>t&&n)continue;m.dataPoints[s].label&&(a.axisX.labels[c]=m.dataPoints[s].label);cb.viewPortMax&&(b.viewPortMax=c);if(b.viewPortMin===c&&e)for(p=0;pd.viewPortMax&&(d.viewPortMax=k))}}m.axisX.valueType=m.xValueType=l?"dateTime":"number"}};p.prototype._processSpecificPlotUnit=function(a){if("waterfall"=== +a.type&&a.dataSeriesIndexes&&!(1>a.dataSeriesIndexes.length))for(var d=a.axisY.dataInfo,b=a.axisX.dataInfo,c,e,g=!1,k=0;kb.max&&(b.max=c),l.dataPointEOs[w].cumulativeSumd.max&&(d.max=l.dataPointEOs[w].cumulativeSum),0q&&(q=1/q),b.minDiff>q&&1!==q&&(b.minDiff=q)):(q=c-l.dataPoints[w-1].x,0>q&&(q*=-1),b.minDiff>q&&0!==q&&(b.minDiff=q)),null!==e&&null!==l.dataPoints[w-1].y&&(a.axisY.logarithmic?(e=l.dataPointEOs[w].cumulativeSum/l.dataPointEOs[w-1].cumulativeSum,1>e&&(e=1/e),d.minDiff>e&&1!==e&&(d.minDiff=e)):(e=l.dataPointEOs[w].cumulativeSum-l.dataPointEOs[w-1].cumulativeSum,0>e&& +(e*=-1),d.minDiff>e&&0!==e&&(d.minDiff=e)))),!(cf&&!s)s=!0;else if(c>f&&s)continue;l.dataPoints[w].label&&(a.axisX.labels[c]=l.dataPoints[w].label);cb.viewPortMax&&(b.viewPortMax=c);0d.viewPortMax&&(d.viewPortMax=l.dataPointEOs[w-1].cumulativeSum));l.dataPointEOs[w].cumulativeSum< +d.viewPortMin&&(d.viewPortMin=l.dataPointEOs[w].cumulativeSum);l.dataPointEOs[w].cumulativeSum>d.viewPortMax&&(d.viewPortMax=l.dataPointEOs[w].cumulativeSum)}l.axisX.valueType=l.xValueType=g?"dateTime":"number"}};p.prototype.calculateAutoBreaks=function(){function a(a,c,b,e){if(e)return b=Math.pow(Math.min(b*a/c,c/a),0.2),1>=b&&(b=Math.pow(1>a?1/a:Math.min(c/a,a),0.25)),{startValue:a*b,endValue:c/b};b=0.2*Math.min(b-c+a,c-a);0>=b&&(b=0.25*Math.min(c-a,Math.abs(a)));return{startValue:a+b,endValue:c- +b}}function d(a){if(a.dataSeriesIndexes&&!(1>a.dataSeriesIndexes.length)){var c=a.axisX.scaleBreaks&&a.axisX.scaleBreaks.autoCalculate&&1<=a.axisX.scaleBreaks.maxNumberOfAutoBreaks,b=a.axisY.scaleBreaks&&a.axisY.scaleBreaks.autoCalculate&&1<=a.axisY.scaleBreaks.maxNumberOfAutoBreaks;if(c||b)for(var d=a.axisY.dataInfo,f=a.axisX.dataInfo,g,m=f.min,l=f.max,k=d.min,n=d.max,f=f._dataRanges,d=d._dataRanges,q,w=0,s=0;sh.dataPoints.length))for(w= +0;wf[q].max&&(f[q].max=g)),b){var r=(n+1-k)*Math.max(parseFloat(a.axisY.scaleBreaks.collapsibleThreshold)||10,10)/100;if((g="waterfall"===a.type?h.dataPointEOs[w].cumulativeSum:h.dataPoints[w].y)&&g.length)for(var p=0;pd[q].max&&(d[q].max=g[p]);else u(g)||(q=Math.floor((g-k)/r),gd[q].max&&(d[q].max=g))}}}}function b(a){if(a.dataSeriesIndexes&&!(1>a.dataSeriesIndexes.length)&&a.axisX.scaleBreaks&&a.axisX.scaleBreaks.autoCalculate&&1<=a.axisX.scaleBreaks.maxNumberOfAutoBreaks)for(var c=a.axisX.dataInfo,b=c.min,d=c.max,f=c._dataRanges,g,m=0,l=0;lk.dataPoints.length))for(m=0;mf[g].max&&(f[g].max=c)}}for(var c,e=this,g=!1,k=0;ks[f].max&&(s[f].max=q)}delete this._axes[k].dataInfo.dataPointYPositiveSums}if(this._axes[k].dataInfo.dataPointYNegativeSums){n=this._axes[k].dataInfo.dataPointYNegativeSums;s=m;for(l in n)n.hasOwnProperty(l)&&!isNaN(l)&&(q=-1*n[l],u(q)||(f=Math.floor((q-w)/c),qs[f].max&&(s[f].max=q)));delete this._axes[k].dataInfo.dataPointYNegativeSums}for(l=0;lc&&g.push({diff:q,start:s,end:w});break}else l++;if(this._axes[k].scaleBreaks.customBreaks)for(l=0;l=e.x1&&(a<=e.x2&&d>=e.y1&&d<=e.y2)&&(c=e.id)}return c};p.prototype.getAutoFontSize=lb;p.prototype.resetOverlayedCanvas=function(){this.overlaidCanvasCtx.clearRect(0, +0,this.width,this.height)};p.prototype.clearCanvas=kb;p.prototype.attachEvent=function(a){this._events.push(a)};p.prototype._touchEventHandler=function(a){if(a.changedTouches&&this.interactivityEnabled){var d=[],b=a.changedTouches,c=b?b[0]:a,e=null;switch(a.type){case "touchstart":case "MSPointerDown":d=["mousemove","mousedown"];this._lastTouchData=Ra(c);this._lastTouchData.time=new Date;break;case "touchmove":case "MSPointerMove":d=["mousemove"];break;case "touchend":case "MSPointerUp":var g=this._lastTouchData&& +this._lastTouchData.time?new Date-this._lastTouchData.time:0,d="touchstart"===this._lastTouchEventType||"MSPointerDown"===this._lastTouchEventType||300>g?["mouseup","click"]:["mouseup"];break;default:return}if(!(b&&1g)this._lastTouchData.scroll=!0}catch(l){}this._lastTouchEventType=a.type;if(this._lastTouchData.scroll&&this.zoomEnabled)this.isDrag&& +this.resetOverlayedCanvas(),this.isDrag=!1;else for(b=0;b=e.x1&&d.x<=e.x2&&d.y>=e.y1&&d.y<=e.y2){c[b].call(c.context,d.x,d.y);"mousedown"===b&&!0===c.capture?(p.capturedEventParam=c,this.overlaidCanvas.setCapture?this.overlaidCanvas.setCapture():document.documentElement.addEventListener("mouseup",this._mouseEventHandler,!1)):"mouseup"===b&&(c.chart.overlaidCanvas.releaseCapture? +c.chart.overlaidCanvas.releaseCapture():document.documentElement.removeEventListener("mouseup",this._mouseEventHandler,!1));break}else c=null;a.target.style.cursor=c&&c.cursor?c.cursor:this._defaultCursor}b=this.plotArea;if(d.xb.x2||d.yb.y2)this.toolTip&&this.toolTip.enabled?this.toolTip.hide():this.resetOverlayedCanvas();this.isDrag&&this.zoomEnabled||!this._eventManager||this._eventManager.mouseEventHandler(a)}};p.prototype._plotAreaMouseDown=function(a,d){this.isDrag=!0;this.dragStartPoint= +{x:a,y:d}};p.prototype._plotAreaMouseUp=function(a,d){if(("normal"===this.plotInfo.axisPlacement||"xySwapped"===this.plotInfo.axisPlacement)&&this.isDrag){var b=d-this.dragStartPoint.y,c=a-this.dragStartPoint.x,e=0<=this.zoomType.indexOf("x"),g=0<=this.zoomType.indexOf("y"),k=!1;this.resetOverlayedCanvas();if("xySwapped"===this.plotInfo.axisPlacement)var l=g,g=e,e=l;if(this.panEnabled||this.zoomEnabled){if(this.panEnabled)for(e=g=0;eb.maximum&&(g=b.viewportMaximum/b.maximum,b.sessionVariables.newViewportMinimum=b.viewportMinimum/g,b.sessionVariables.newViewportMaximum=b.viewportMaximum/g,k=!0):b.viewportMinimumb.maximum&&(g=b.viewportMaximum-b.maximum,b.sessionVariables.newViewportMinimum=b.viewportMinimum-g,b.sessionVariables.newViewportMaximum=b.viewportMaximum-g,k=!0);else if((!e||2Math.abs(b)&&(this.panEnabled||this.zoomEnabled)?this.toolTip.hide():this.panEnabled||this.zoomEnabled||this.toolTip.mouseMoveHandler(a,d);if((!e||2f)var B=f,f=n,n=B;if(q.scaleBreaks)for(B=0;!g&&B=f;if(isFinite(q.dataInfo.minDiff))if(B=q.getApparentDifference(n,f,null,!0),!(g||!(this.panEnabled&&q.scaleBreaks&&q.scaleBreaks._appliedBreaks.length)&&(q.logarithmic&&Bq.maximum))w.push(q),s.push({val1:n,val2:f}),l= +!0;else if(!e){l=!1;break}}return{isValid:l,axesWithValidRange:w,axesRanges:s}};p.prototype.preparePlotArea=function(){var a=this.plotArea;!r&&(0b.lineCoordinates.x2?d.x2:b.lineCoordinates.x2;a.y2=d.y2>d.y1?d.y2:b.lineCoordinates.y2;a.width=a.x2-a.x1;a.height=a.y2-a.y1}this.axisY2&&0b.lineCoordinates.x2?d.x2:b.lineCoordinates.x2,a.y2=d.y2>d.y1?d.y2:b.lineCoordinates.y2,a.width=a.x2-a.x1,a.height=a.y2-a.y1)}else d=this.layoutManager.getFreeSpace(),a.x1=d.x1,a.x2=d.x2,a.y1= +d.y1,a.y2=d.y2,a.width=d.width,a.height=d.height;r||(a.canvas.width=a.width,a.canvas.height=a.height,a.canvas.style.left=a.x1+"px",a.canvas.style.top=a.y1+"px",(0b.x2||m.point.yb.y2+1)continue}else if("rangearea"===s||"rangesplinearea"===s){if(m.dataPoint.xy.viewportMaximum||Math.max.apply(null,m.dataPoint.y)A.viewportMaximum)continue}else if(0<=s.indexOf("line")||0<=s.indexOf("area")||0<=s.indexOf("bubble")||0<=s.indexOf("scatter")){if(m.dataPoint.xy.viewportMaximum|| +m.dataPoint.yA.viewportMaximum)continue}else if(0<=s.indexOf("column")||"waterfall"===s||"error"===s&&!m.axisSwapped){if(m.dataPoint.xy.viewportMaximum||m.bounds.y1>b.y2||m.bounds.y2y.viewportMaximum||m.bounds.x1>b.x2||m.bounds.x2 +y.viewportMaximum||Math.max.apply(null,m.dataPoint.y)A.viewportMaximum)continue}else if(m.dataPoint.xy.viewportMaximum)continue;e=k=2;"horizontal"===C?(l=f.width,w=f.height):(w=f.width,l=f.height);if("normal"===this.plotInfo.axisPlacement){if(0<=s.indexOf("line")||0<=s.indexOf("area"))t="auto",k=4;else if(0<=s.indexOf("stacked"))"auto"===t&&(t="inside");else if("bubble"===s||"scatter"===s)t="inside";q=m.point.x- +l/2;"inside"!==t?(e=b.y1,g=b.y2,0m.point.y)):(n=m.point.y+k+c,n>g-w-k-c&&(n="auto"===t?Math.min(m.point.y,g)-w-k-c:g-w-k-c,v=ng-w-k&&("bubble"===s||"scatter"===s)&&(n=Math.min(m.point.y+k,b.y2-w-k))),n=Math.min(n,g-w))}else 0<=s.indexOf("line")||0<=s.indexOf("area")||0<=s.indexOf("scatter")?(t="auto",e=4):0<=s.indexOf("stacked")?"auto"===t&&(t="inside"):"bubble"===s&&(t="inside"),n=m.point.y-w/2,"inside"!==t?(k=b.x1,g=b.x2,0>ma?(q=m.point.x-l-e-c,qm.point.x)):(q=m.point.x+e+c,q>g-l-e-c&&(q="auto"=== +t?Math.min(m.point.x,g)-l-e-c:g-l-e-c,v=qma?Math.max(m.bounds.x1,b.x1)+l/2+e:Math.min(m.bounds.x2,b.x2)-l/2-e:(Math.max(m.bounds.x1,b.x1)+Math.min(m.bounds.x2,b.x2))/2,q=0>ma?Math.max(m.point.x,c)-l/2:Math.min(m.point.x,c)-l/2,q=Math.max(q,k));"vertical"===C&&(n+=w);f.x=q;f.y=n;f.render(!0);p&&("inside"!==t&&(0>s.indexOf("bar")&&("error"!==s||!m.axisSwapped)&&m.point.x>b.x1&&m.point.xs.indexOf("column")&&("error"!==s||m.axisSwapped)&&m.point.y>b.y1&&m.point.y=a.dataSeriesIndexes.length)){var c= +this._eventManager.ghostCtx;b.save();var e=this.plotArea;b.beginPath();b.rect(e.x1,e.y1,e.width,e.height);b.clip();for(var g=[],k,l=0;la.axisX.dataInfo.viewPortMax&&(!m.connectNullData||!u)))if("number"!==typeof s[t].y)0s[t].y===a.axisY.reversed?1:-1,color:B})}b.stroke();r&&c.stroke()}}ia.drawMarkers(g);r&&(d.drawImage(this._preRenderCanvas,0,0,this.width,this.height),b.globalCompositeOperation="source-atop",a.axisX.maskCanvas&& +b.drawImage(a.axisX.maskCanvas,0,0,this.width,this.height),a.axisY.maskCanvas&&b.drawImage(a.axisY.maskCanvas,0,0,this.width,this.height),this._breaksCanvasCtx&&this._breaksCanvasCtx.drawImage(this._preRenderCanvas,0,0,this.width,this.height),b.clearRect(e.x1,e.y1,e.width,e.height),c.beginPath());b.restore();b.beginPath();return{source:d,dest:this.plotArea.ctx,animationCallback:M.xClipAnimation,easingFunction:M.easing.linear,animationBase:0}}};p.prototype.renderStepLine=function(a){var d=a.targetCanvasCtx|| +this.plotArea.ctx,b=r?this._preRenderCtx:d;if(!(0>=a.dataSeriesIndexes.length)){var c=this._eventManager.ghostCtx;b.save();var e=this.plotArea;b.beginPath();b.rect(e.x1,e.y1,e.width,e.height);b.clip();for(var g=[],k,l=0;la.axisX.dataInfo.viewPortMax&&(!m.connectNullData||!u)))if("number"!==typeof s[t].y)0s[t].y===a.axisY.reversed?1:-1,color:B})}b.stroke();r&&c.stroke()}}ia.drawMarkers(g);r&& +(d.drawImage(this._preRenderCanvas,0,0,this.width,this.height),b.globalCompositeOperation="source-atop",a.axisX.maskCanvas&&b.drawImage(a.axisX.maskCanvas,0,0,this.width,this.height),a.axisY.maskCanvas&&b.drawImage(a.axisY.maskCanvas,0,0,this.width,this.height),this._breaksCanvasCtx&&this._breaksCanvasCtx.drawImage(this._preRenderCanvas,0,0,this.width,this.height),b.clearRect(e.x1,e.y1,e.width,e.height),c.beginPath());b.restore();b.beginPath();return{source:d,dest:this.plotArea.ctx,animationCallback:M.xClipAnimation, +easingFunction:M.easing.linear,animationBase:0}}};p.prototype.renderSpline=function(a){function d(a){a=v(a,2);if(0=a.dataSeriesIndexes.length)){var e=this._eventManager.ghostCtx;c.save();var g=this.plotArea;c.beginPath(); +c.rect(g.x1,g.y1,g.width,g.height);c.clip();for(var k=[],l=0;la.axisX.dataInfo.viewPortMax&&(!m.connectNullData||!u)))if("number"!==typeof s[p].y)0s[p].y===a.axisY.reversed?1:-1,color:B});u=!1}d(x)}ia.drawMarkers(k);r&&(b.drawImage(this._preRenderCanvas, +0,0,this.width,this.height),c.globalCompositeOperation="source-atop",a.axisX.maskCanvas&&c.drawImage(a.axisX.maskCanvas,0,0,this.width,this.height),a.axisY.maskCanvas&&c.drawImage(a.axisY.maskCanvas,0,0,this.width,this.height),this._breaksCanvasCtx&&this._breaksCanvasCtx.drawImage(this._preRenderCanvas,0,0,this.width,this.height),c.clearRect(g.x1,g.y1,g.width,g.height),e.beginPath());c.restore();c.beginPath();return{source:b,dest:this.plotArea.ctx,animationCallback:M.xClipAnimation,easingFunction:M.easing.linear, +animationBase:0}}};p.prototype.renderColumn=function(a){var d=a.targetCanvasCtx||this.plotArea.ctx,b=r?this._preRenderCtx:d;if(!(0>=a.dataSeriesIndexes.length)){var c=null,e=this.plotArea,g=0,k,l,w,m=a.axisY.convertValueToPixel(a.axisY.logarithmic?a.axisY.viewportMinimum:0),g=this.options.dataPointMinWidth?this.dataPointMinWidth:this.options.dataPointWidth?this.dataPointWidth:1,s=this.options.dataPointMaxWidth?this.dataPointMaxWidth:this.options.dataPointWidth?this.dataPointWidth:Math.min(0.15*this.width, +0.9*(this.plotArea.width/a.plotType.totalDataSeries))<<0,q=a.axisX.dataInfo.minDiff;isFinite(q)||(q=0.3*Math.abs(a.axisX.range));q=this.dataPointWidth=this.options.dataPointWidth?this.dataPointWidth:0.9*(e.width*(a.axisX.logarithmic?Math.log(q)/Math.log(a.axisX.range):Math.abs(q)/Math.abs(a.axisX.range))/a.plotType.totalDataSeries)<<0;this.dataPointMaxWidth&&g>s&&(g=Math.min(this.options.dataPointWidth?this.dataPointWidth:Infinity,s));!this.dataPointMaxWidth&&(this.dataPointMinWidth&&ss&&(q=s);b.save();r&&this._eventManager.ghostCtx.save();b.beginPath();b.rect(e.x1,e.y1,e.width,e.height);b.clip();r&&(this._eventManager.ghostCtx.beginPath(),this._eventManager.ghostCtx.rect(e.x1,e.y1,e.width,e.height),this._eventManager.ghostCtx.clip());for(s=0;sa.axisX.dataInfo.viewPortMax)&&"number"===typeof B[g].y){k=a.axisX.convertValueToPixel(w);l=a.axisY.convertValueToPixel(B[g].y);k=a.axisX.reversed?k+a.plotType.totalDataSeries*q/2-(a.previousDataSeriesCount+s)*q<<0:k-a.plotType.totalDataSeries*q/2+(a.previousDataSeriesCount+s)*q<<0;var h=a.axisX.reversed?k-q<<0:k+q<<0,t;0<=B[g].y?t=m:(t=l,l=m);l>t&&(c=l,l=t,t=c);c=B[g].color?B[g].color:f._colorSet[g%f._colorSet.length];ea(b,k,l,h,t,c,0,null,p&&0<=B[g].y, +0>B[g].y&&p,!1,!1,f.fillOpacity);c=f.dataPointIds[g];this._eventManager.objectMap[c]={id:c,objectType:"dataPoint",dataSeriesIndex:n,dataPointIndex:g,x1:k,y1:l,x2:h,y2:t};c=N(c);r&&ea(this._eventManager.ghostCtx,k,l,h,t,c,0,null,!1,!1,!1,!1);(B[g].indexLabel||f.indexLabel||B[g].indexLabelFormatter||f.indexLabelFormatter)&&this._indexLabels.push({chartType:"column",dataPoint:B[g],dataSeries:f,point:{x:k+(h-k)/2,y:0>B[g].y===a.axisY.reversed?l:t},direction:0>B[g].y===a.axisY.reversed?1:-1,bounds:{x1:k, +y1:Math.min(l,t),x2:h,y2:Math.max(l,t)},color:c})}}r&&(d.drawImage(this._preRenderCanvas,0,0,this.width,this.height),b.globalCompositeOperation="source-atop",a.axisX.maskCanvas&&b.drawImage(a.axisX.maskCanvas,0,0,this.width,this.height),a.axisY.maskCanvas&&b.drawImage(a.axisY.maskCanvas,0,0,this.width,this.height),this._breaksCanvasCtx&&this._breaksCanvasCtx.drawImage(this._preRenderCanvas,0,0,this.width,this.height),b.clearRect(e.x1,e.y1,e.width,e.height),this._eventManager.ghostCtx.restore());b.restore(); +return{source:d,dest:this.plotArea.ctx,animationCallback:M.yScaleAnimation,easingFunction:M.easing.easeOutQuart,animationBase:ma.axisY.bounds.y2?a.axisY.bounds.y2:m}}};p.prototype.renderStackedColumn=function(a){var d=a.targetCanvasCtx||this.plotArea.ctx,b=r?this._preRenderCtx:d;if(!(0>=a.dataSeriesIndexes.length)){var c=null,e=this.plotArea,g=[],k=[],l=[],w=[],m=0,s,q,n=a.axisY.convertValueToPixel(a.axisY.logarithmic?a.axisY.viewportMinimum:0),m=this.options.dataPointMinWidth? +this.dataPointMinWidth:this.options.dataPointWidth?this.dataPointWidth:1;s=this.options.dataPointMaxWidth?this.dataPointMaxWidth:this.options.dataPointWidth?this.dataPointWidth:0.15*this.width<<0;var f=a.axisX.dataInfo.minDiff;isFinite(f)||(f=0.3*Math.abs(a.axisX.range));f=this.options.dataPointWidth?this.dataPointWidth:0.9*(e.width*(a.axisX.logarithmic?Math.log(f)/Math.log(a.axisX.range):Math.abs(f)/Math.abs(a.axisX.range))/a.plotType.plotUnits.length)<<0;this.dataPointMaxWidth&&m>s&&(m=Math.min(this.options.dataPointWidth? +this.dataPointWidth:Infinity,s));!this.dataPointMaxWidth&&(this.dataPointMinWidth&&ss&&(f=s);b.save();r&&this._eventManager.ghostCtx.save();b.beginPath();b.rect(e.x1,e.y1,e.width,e.height);b.clip();r&&(this._eventManager.ghostCtx.beginPath(),this._eventManager.ghostCtx.rect(e.x1,e.y1,e.width,e.height),this._eventManager.ghostCtx.clip());for(var B=0;Ba.axisX.dataInfo.viewPortMax)&&"number"===typeof t[m].y){s=a.axisX.convertValueToPixel(c);var x=s-a.plotType.plotUnits.length*f/2+a.index*f<<0,v=x+f<<0,y;if(a.axisY.logarithmic||a.axisY.scaleBreaks&&0=t[m].y)w[c]=t[m].y+(w[c]?w[c]:0),y=a.axisY.convertValueToPixel(w[c]),q="undefined"!==typeof k[c]?k[c]:n,k[c]=y;else if(q=a.axisY.convertValueToPixel(t[m].y),0<=t[m].y){var A="undefined"!==typeof g[c]?g[c]:0;q-=A;y=n-A;g[c]=A+(y-q)}else A=k[c]?k[c]:0,y=q+A,q=n+A,k[c]=A+(y-q);c=t[m].color?t[m].color:p._colorSet[m%p._colorSet.length];ea(b,x,q,v,y,c,0,null,u&&0<=t[m].y,0>t[m].y&&u,!1, +!1,p.fillOpacity);c=p.dataPointIds[m];this._eventManager.objectMap[c]={id:c,objectType:"dataPoint",dataSeriesIndex:h,dataPointIndex:m,x1:x,y1:q,x2:v,y2:y};c=N(c);r&&ea(this._eventManager.ghostCtx,x,q,v,y,c,0,null,!1,!1,!1,!1);(t[m].indexLabel||p.indexLabel||t[m].indexLabelFormatter||p.indexLabelFormatter)&&this._indexLabels.push({chartType:"stackedColumn",dataPoint:t[m],dataSeries:p,point:{x:s,y:0<=t[m].y?q:y},direction:0>t[m].y===a.axisY.reversed?1:-1,bounds:{x1:x,y1:Math.min(q,y),x2:v,y2:Math.max(q, +y)},color:c})}}}r&&(d.drawImage(this._preRenderCanvas,0,0,this.width,this.height),b.globalCompositeOperation="source-atop",a.axisX.maskCanvas&&b.drawImage(a.axisX.maskCanvas,0,0,this.width,this.height),a.axisY.maskCanvas&&b.drawImage(a.axisY.maskCanvas,0,0,this.width,this.height),this._breaksCanvasCtx&&this._breaksCanvasCtx.drawImage(this._preRenderCanvas,0,0,this.width,this.height),b.clearRect(e.x1,e.y1,e.width,e.height),this._eventManager.ghostCtx.restore());b.restore();return{source:d,dest:this.plotArea.ctx, +animationCallback:M.yScaleAnimation,easingFunction:M.easing.easeOutQuart,animationBase:na.axisY.bounds.y2?a.axisY.bounds.y2:n}}};p.prototype.renderStackedColumn100=function(a){var d=a.targetCanvasCtx||this.plotArea.ctx,b=r?this._preRenderCtx:d;if(!(0>=a.dataSeriesIndexes.length)){var c=null,e=this.plotArea,g=[],k=[],l=[],w=[],m=0,s,q,n=a.axisY.convertValueToPixel(a.axisY.logarithmic?a.axisY.viewportMinimum:0),m=this.options.dataPointMinWidth?this.dataPointMinWidth: +this.options.dataPointWidth?this.dataPointWidth:1;s=this.options.dataPointMaxWidth?this.dataPointMaxWidth:this.options.dataPointWidth?this.dataPointWidth:0.15*this.width<<0;var f=a.axisX.dataInfo.minDiff;isFinite(f)||(f=0.3*Math.abs(a.axisX.range));f=this.options.dataPointWidth?this.dataPointWidth:0.9*(e.width*(a.axisX.logarithmic?Math.log(f)/Math.log(a.axisX.range):Math.abs(f)/Math.abs(a.axisX.range))/a.plotType.plotUnits.length)<<0;this.dataPointMaxWidth&&m>s&&(m=Math.min(this.options.dataPointWidth? +this.dataPointWidth:Infinity,s));!this.dataPointMaxWidth&&(this.dataPointMinWidth&&ss&&(f=s);b.save();r&&this._eventManager.ghostCtx.save();b.beginPath();b.rect(e.x1,e.y1,e.width,e.height);b.clip();r&&(this._eventManager.ghostCtx.beginPath(),this._eventManager.ghostCtx.rect(e.x1,e.y1,e.width,e.height),this._eventManager.ghostCtx.clip());for(var B=0;Ba.axisX.dataInfo.viewPortMax)&&"number"===typeof t[m].y){s=a.axisX.convertValueToPixel(c);q=0!==a.dataPointYSums[c]?100*(t[m].y/a.dataPointYSums[c]):0;var x=s-a.plotType.plotUnits.length*f/2+a.index*f<<0,v=x+f<<0,y;if(a.axisY.logarithmic||a.axisY.scaleBreaks&&0=l[c])continue;q=a.axisY.convertValueToPixel(l[c]);y=g[c]?g[c]:n;g[c]=q}else if(a.axisY.scaleBreaks&&0=t[m].y)w[c]=q+("undefined"!==typeof w[c]?w[c]:0),y=a.axisY.convertValueToPixel(w[c]),q=k[c]?k[c]:n,k[c]=y;else if(q=a.axisY.convertValueToPixel(q),0<=t[m].y){var A="undefined"!==typeof g[c]?g[c]:0;q-=A;y=n-A;a.dataSeriesIndexes.length-1===B&&1>=Math.abs(e.y1-q)&&(q=e.y1);g[c]=A+(y-q)}else A="undefined"!==typeof k[c]? +k[c]:0,y=q+A,q=n+A,a.dataSeriesIndexes.length-1===B&&1>=Math.abs(e.y2-y)&&(y=e.y2),k[c]=A+(y-q);c=t[m].color?t[m].color:h._colorSet[m%h._colorSet.length];ea(b,x,q,v,y,c,0,null,u&&0<=t[m].y,0>t[m].y&&u,!1,!1,h.fillOpacity);c=h.dataPointIds[m];this._eventManager.objectMap[c]={id:c,objectType:"dataPoint",dataSeriesIndex:p,dataPointIndex:m,x1:x,y1:q,x2:v,y2:y};c=N(c);r&&ea(this._eventManager.ghostCtx,x,q,v,y,c,0,null,!1,!1,!1,!1);(t[m].indexLabel||h.indexLabel||t[m].indexLabelFormatter||h.indexLabelFormatter)&& +this._indexLabels.push({chartType:"stackedColumn100",dataPoint:t[m],dataSeries:h,point:{x:s,y:0<=t[m].y?q:y},direction:0>t[m].y===a.axisY.reversed?1:-1,bounds:{x1:x,y1:Math.min(q,y),x2:v,y2:Math.max(q,y)},color:c})}}r&&(d.drawImage(this._preRenderCanvas,0,0,this.width,this.height),b.globalCompositeOperation="source-atop",a.axisX.maskCanvas&&b.drawImage(a.axisX.maskCanvas,0,0,this.width,this.height),a.axisY.maskCanvas&&b.drawImage(a.axisY.maskCanvas,0,0,this.width,this.height),this._breaksCanvasCtx&& +this._breaksCanvasCtx.drawImage(this._preRenderCanvas,0,0,this.width,this.height),b.clearRect(e.x1,e.y1,e.width,e.height),this._eventManager.ghostCtx.restore());b.restore();return{source:d,dest:this.plotArea.ctx,animationCallback:M.yScaleAnimation,easingFunction:M.easing.easeOutQuart,animationBase:na.axisY.bounds.y2?a.axisY.bounds.y2:n}}};p.prototype.renderBar=function(a){var d=a.targetCanvasCtx||this.plotArea.ctx,b=r?this._preRenderCtx:d;if(!(0>=a.dataSeriesIndexes.length)){var c= +null,e=this.plotArea,g=0,k,l,w,m=a.axisY.convertValueToPixel(a.axisY.logarithmic?a.axisY.viewportMinimum:0),g=this.options.dataPointMinWidth?this.dataPointMinWidth:this.options.dataPointWidth?this.dataPointWidth:1,s=this.options.dataPointMaxWidth?this.dataPointMaxWidth:this.options.dataPointWidth?this.dataPointWidth:Math.min(0.15*this.height,0.9*(this.plotArea.height/a.plotType.totalDataSeries))<<0,q=a.axisX.dataInfo.minDiff;isFinite(q)||(q=0.3*Math.abs(a.axisX.range));q=this.options.dataPointWidth? +this.dataPointWidth:0.9*(e.height*(a.axisX.logarithmic?Math.log(q)/Math.log(a.axisX.range):Math.abs(q)/Math.abs(a.axisX.range))/a.plotType.totalDataSeries)<<0;this.dataPointMaxWidth&&g>s&&(g=Math.min(this.options.dataPointWidth?this.dataPointWidth:Infinity,s));!this.dataPointMaxWidth&&(this.dataPointMinWidth&&ss&&(q=s);b.save();r&&this._eventManager.ghostCtx.save();b.beginPath();b.rect(e.x1,e.y1,e.width,e.height); +b.clip();r&&(this._eventManager.ghostCtx.beginPath(),this._eventManager.ghostCtx.rect(e.x1,e.y1,e.width,e.height),this._eventManager.ghostCtx.clip());for(s=0;sa.axisX.dataInfo.viewPortMax)&&"number"===typeof B[g].y){l=a.axisX.convertValueToPixel(w); +k=a.axisY.convertValueToPixel(B[g].y);l=a.axisX.reversed?l+a.plotType.totalDataSeries*q/2-(a.previousDataSeriesCount+s)*q<<0:l-a.plotType.totalDataSeries*q/2+(a.previousDataSeriesCount+s)*q<<0;var p=a.axisX.reversed?l-q<<0:l+q<<0,t;0<=B[g].y?t=m:(t=k,k=m);c=B[g].color?B[g].color:f._colorSet[g%f._colorSet.length];ea(b,t,l,k,p,c,0,null,h,!1,!1,!1,f.fillOpacity);c=f.dataPointIds[g];this._eventManager.objectMap[c]={id:c,objectType:"dataPoint",dataSeriesIndex:n,dataPointIndex:g,x1:t,y1:l,x2:k,y2:p};c= +N(c);r&&ea(this._eventManager.ghostCtx,t,l,k,p,c,0,null,!1,!1,!1,!1);(B[g].indexLabel||f.indexLabel||B[g].indexLabelFormatter||f.indexLabelFormatter)&&this._indexLabels.push({chartType:"bar",dataPoint:B[g],dataSeries:f,point:{x:0<=B[g].y?k:t,y:l+(p-l)/2},direction:0>B[g].y===a.axisY.reversed?1:-1,bounds:{x1:Math.min(t,k),y1:l,x2:Math.max(t,k),y2:p},color:c})}}}r&&(d.drawImage(this._preRenderCanvas,0,0,this.width,this.height),b.globalCompositeOperation="source-atop",a.axisX.maskCanvas&&b.drawImage(a.axisX.maskCanvas, +0,0,this.width,this.height),a.axisY.maskCanvas&&b.drawImage(a.axisY.maskCanvas,0,0,this.width,this.height),this._breaksCanvasCtx&&this._breaksCanvasCtx.drawImage(this._preRenderCanvas,0,0,this.width,this.height),b.clearRect(e.x1,e.y1,e.width,e.height),this._eventManager.ghostCtx.restore());b.restore();return{source:d,dest:this.plotArea.ctx,animationCallback:M.xScaleAnimation,easingFunction:M.easing.easeOutQuart,animationBase:ma.axisY.bounds.x2?a.axisY.bounds.x2: +m}}};p.prototype.renderStackedBar=function(a){var d=a.targetCanvasCtx||this.plotArea.ctx,b=r?this._preRenderCtx:d;if(!(0>=a.dataSeriesIndexes.length)){var c=null,e=this.plotArea,g=[],k=[],l=[],w=[],m=0,s,q,n=a.axisY.convertValueToPixel(a.axisY.logarithmic?a.axisY.viewportMinimum:0),m=this.options.dataPointMinWidth?this.dataPointMinWidth:this.options.dataPointWidth?this.dataPointWidth:1;q=this.options.dataPointMaxWidth?this.dataPointMaxWidth:this.options.dataPointWidth?this.dataPointWidth:0.15*this.height<< +0;var f=a.axisX.dataInfo.minDiff;isFinite(f)||(f=0.3*Math.abs(a.axisX.range));f=this.options.dataPointWidth?this.dataPointWidth:0.9*(e.height*(a.axisX.logarithmic?Math.log(f)/Math.log(a.axisX.range):Math.abs(f)/Math.abs(a.axisX.range))/a.plotType.plotUnits.length)<<0;this.dataPointMaxWidth&&m>q&&(m=Math.min(this.options.dataPointWidth?this.dataPointWidth:Infinity,q));!this.dataPointMaxWidth&&(this.dataPointMinWidth&&qq&&(f=q);b.save();r&&this._eventManager.ghostCtx.save();b.beginPath();b.rect(e.x1,e.y1,e.width,e.height);b.clip();r&&(this._eventManager.ghostCtx.beginPath(),this._eventManager.ghostCtx.rect(e.x1,e.y1,e.width,e.height),this._eventManager.ghostCtx.clip());for(var B=0;Ba.axisX.dataInfo.viewPortMax)&&"number"===typeof t[m].y){q=a.axisX.convertValueToPixel(c);var x=q-a.plotType.plotUnits.length*f/2+a.index*f<<0,v=x+f<<0,y;if(a.axisY.logarithmic||a.axisY.scaleBreaks&&0=t[m].y)w[c]=t[m].y+(w[c]?w[c]:0),s=k[c]? +k[c]:n,k[c]=y=a.axisY.convertValueToPixel(w[c]);else if(s=a.axisY.convertValueToPixel(t[m].y),0<=t[m].y){var A=g[c]?g[c]:0;y=n+A;s+=A;g[c]=A+(s-y)}else A=k[c]?k[c]:0,y=s-A,s=n-A,k[c]=A+(s-y);c=t[m].color?t[m].color:h._colorSet[m%h._colorSet.length];ea(b,y,x,s,v,c,0,null,u,!1,!1,!1,h.fillOpacity);c=h.dataPointIds[m];this._eventManager.objectMap[c]={id:c,objectType:"dataPoint",dataSeriesIndex:p,dataPointIndex:m,x1:y,y1:x,x2:s,y2:v};c=N(c);r&&ea(this._eventManager.ghostCtx,y,x,s,v,c,0,null,!1,!1,!1, +!1);(t[m].indexLabel||h.indexLabel||t[m].indexLabelFormatter||h.indexLabelFormatter)&&this._indexLabels.push({chartType:"stackedBar",dataPoint:t[m],dataSeries:h,point:{x:0<=t[m].y?s:y,y:q},direction:0>t[m].y===a.axisY.reversed?1:-1,bounds:{x1:Math.min(y,s),y1:x,x2:Math.max(y,s),y2:v},color:c})}}}r&&(d.drawImage(this._preRenderCanvas,0,0,this.width,this.height),b.globalCompositeOperation="source-atop",a.axisX.maskCanvas&&b.drawImage(a.axisX.maskCanvas,0,0,this.width,this.height),a.axisY.maskCanvas&& +b.drawImage(a.axisY.maskCanvas,0,0,this.width,this.height),this._breaksCanvasCtx&&this._breaksCanvasCtx.drawImage(this._preRenderCanvas,0,0,this.width,this.height),b.clearRect(e.x1,e.y1,e.width,e.height),this._eventManager.ghostCtx.restore());b.restore();return{source:d,dest:this.plotArea.ctx,animationCallback:M.xScaleAnimation,easingFunction:M.easing.easeOutQuart,animationBase:na.axisY.bounds.x2?a.axisY.bounds.x2:n}}};p.prototype.renderStackedBar100=function(a){var d= +a.targetCanvasCtx||this.plotArea.ctx,b=r?this._preRenderCtx:d;if(!(0>=a.dataSeriesIndexes.length)){var c=null,e=this.plotArea,g=[],k=[],l=[],w=[],m=0,s,q,n=a.axisY.convertValueToPixel(a.axisY.logarithmic?a.axisY.viewportMinimum:0),m=this.options.dataPointMinWidth?this.dataPointMinWidth:this.options.dataPointWidth?this.dataPointWidth:1;q=this.options.dataPointMaxWidth?this.dataPointMaxWidth:this.options.dataPointWidth?this.dataPointWidth:0.15*this.height<<0;var f=a.axisX.dataInfo.minDiff;isFinite(f)|| +(f=0.3*Math.abs(a.axisX.range));f=this.options.dataPointWidth?this.dataPointWidth:0.9*(e.height*(a.axisX.logarithmic?Math.log(f)/Math.log(a.axisX.range):Math.abs(f)/Math.abs(a.axisX.range))/a.plotType.plotUnits.length)<<0;this.dataPointMaxWidth&&m>q&&(m=Math.min(this.options.dataPointWidth?this.dataPointWidth:Infinity,q));!this.dataPointMaxWidth&&(this.dataPointMinWidth&&qq&&(f=q);b.save();r&&this._eventManager.ghostCtx.save(); +b.beginPath();b.rect(e.x1,e.y1,e.width,e.height);b.clip();r&&(this._eventManager.ghostCtx.beginPath(),this._eventManager.ghostCtx.rect(e.x1,e.y1,e.width,e.height),this._eventManager.ghostCtx.clip());for(var B=0;Ba.axisX.dataInfo.viewPortMax)&& +"number"===typeof t[m].y){q=a.axisX.convertValueToPixel(c);var x;x=0!==a.dataPointYSums[c]?100*(t[m].y/a.dataPointYSums[c]):0;var v=q-a.plotType.plotUnits.length*f/2+a.index*f<<0,y=v+f<<0;if(a.axisY.logarithmic||a.axisY.scaleBreaks&&0=l[c])continue;x=g[c]?g[c]:n;g[c]=s=a.axisY.convertValueToPixel(l[c])}else if(a.axisY.scaleBreaks&&0=t[m].y)w[c]=x+(w[c]?w[c]:0),s=k[c]?k[c]: +n,k[c]=x=a.axisY.convertValueToPixel(w[c]);else if(s=a.axisY.convertValueToPixel(x),0<=t[m].y){var A=g[c]?g[c]:0;x=n+A;s+=A;a.dataSeriesIndexes.length-1===B&&1>=Math.abs(e.x2-s)&&(s=e.x2);g[c]=A+(s-x)}else A=k[c]?k[c]:0,x=s-A,s=n-A,a.dataSeriesIndexes.length-1===B&&1>=Math.abs(e.x1-x)&&(x=e.x1),k[c]=A+(s-x);c=t[m].color?t[m].color:p._colorSet[m%p._colorSet.length];ea(b,x,v,s,y,c,0,null,u,!1,!1,!1,p.fillOpacity);c=p.dataPointIds[m];this._eventManager.objectMap[c]={id:c,objectType:"dataPoint",dataSeriesIndex:h, +dataPointIndex:m,x1:x,y1:v,x2:s,y2:y};c=N(c);r&&ea(this._eventManager.ghostCtx,x,v,s,y,c,0,null,!1,!1,!1,!1);(t[m].indexLabel||p.indexLabel||t[m].indexLabelFormatter||p.indexLabelFormatter)&&this._indexLabels.push({chartType:"stackedBar100",dataPoint:t[m],dataSeries:p,point:{x:0<=t[m].y?s:x,y:q},direction:0>t[m].y===a.axisY.reversed?1:-1,bounds:{x1:Math.min(x,s),y1:v,x2:Math.max(x,s),y2:y},color:c})}}}r&&(d.drawImage(this._preRenderCanvas,0,0,this.width,this.height),b.globalCompositeOperation="source-atop", +a.axisX.maskCanvas&&b.drawImage(a.axisX.maskCanvas,0,0,this.width,this.height),a.axisY.maskCanvas&&b.drawImage(a.axisY.maskCanvas,0,0,this.width,this.height),this._breaksCanvasCtx&&this._breaksCanvasCtx.drawImage(this._preRenderCanvas,0,0,this.width,this.height),b.clearRect(e.x1,e.y1,e.width,e.height),this._eventManager.ghostCtx.restore());b.restore();return{source:d,dest:this.plotArea.ctx,animationCallback:M.xScaleAnimation,easingFunction:M.easing.easeOutQuart,animationBase:na.axisY.bounds.x2?a.axisY.bounds.x2:n}}};p.prototype.renderArea=function(a){var d,b;function c(){A&&(0=a.axisY.viewportMinimum&&0<=a.axisY.viewportMaximum?y=v:0>a.axisY.viewportMaximum?y=w.y1:0=a.dataSeriesIndexes.length)){var k=this._eventManager.ghostCtx,l=a.axisX.lineCoordinates,w=a.axisY.lineCoordinates,m=[],s=this.plotArea,q;g.save();r&&k.save();g.beginPath();g.rect(s.x1,s.y1,s.width,s.height);g.clip();r&&(k.beginPath(),k.rect(s.x1,s.y1,s.width,s.height),k.clip());for(var n=0;na.axisX.dataInfo.viewPortMax&&(!B.connectNullData||!da)))if("number"!==typeof p[h].y)B.connectNullData||(da||d)||c(),da=!0;else{t=a.axisX.convertValueToPixel(x);u=a.axisY.convertValueToPixel(p[h].y);d||da?(!d&&B.connectNullData?(g.setLineDash&&(B.options.nullDataLineDashType||b===B.lineDashType&&B.lineDashType!==B.nullDataLineDashType)&&(d=t,b=u,t=q.x,u=q.y,c(),g.moveTo(q.x,q.y),t=d,u=b,A=q,b=B.nullDataLineDashType,g.setLineDash(Y)),g.lineTo(t,u),r&&k.lineTo(t, +u)):(g.beginPath(),g.moveTo(t,u),r&&(k.beginPath(),k.moveTo(t,u)),A={x:t,y:u}),da=d=!1):(g.lineTo(t,u),r&&k.lineTo(t,u),0==h%250&&c());q={x:t,y:u};hp[h].y===a.axisY.reversed?1:-1,color:z})}c();ia.drawMarkers(m)}}r&&(e.drawImage(this._preRenderCanvas, +0,0,this.width,this.height),g.globalCompositeOperation="source-atop",a.axisX.maskCanvas&&g.drawImage(a.axisX.maskCanvas,0,0,this.width,this.height),a.axisY.maskCanvas&&g.drawImage(a.axisY.maskCanvas,0,0,this.width,this.height),this._breaksCanvasCtx&&this._breaksCanvasCtx.drawImage(this._preRenderCanvas,0,0,this.width,this.height),g.clearRect(s.x1,s.y1,s.width,s.height),this._eventManager.ghostCtx.restore());g.restore();return{source:e,dest:this.plotArea.ctx,animationCallback:M.xClipAnimation,easingFunction:M.easing.linear, +animationBase:0}}};p.prototype.renderSplineArea=function(a){function d(){var b=v(x,2);if(0=a.axisY.viewportMinimum&&0<=a.axisY.viewportMaximum? +t=p:0>a.axisY.viewportMaximum?t=k.y1:0=a.dataSeriesIndexes.length)){var e=this._eventManager.ghostCtx,g=a.axisX.lineCoordinates,k=a.axisY.lineCoordinates,l=[],w=this.plotArea;c.save();r&& +e.save();c.beginPath();c.rect(w.x1,w.y1,w.width,w.height);c.clip();r&&(e.beginPath(),e.rect(w.x1,w.y1,w.width,w.height),e.clip());for(var m=0;ma.axisX.dataInfo.viewPortMax&&(!q.connectNullData||!h)))if("number"!==typeof n[f].y)0n[f].y===a.axisY.reversed?1:-1,color:ma});h=!1}d();ia.drawMarkers(l)}}r&&(b.drawImage(this._preRenderCanvas,0,0,this.width,this.height),c.globalCompositeOperation="source-atop",a.axisX.maskCanvas&&c.drawImage(a.axisX.maskCanvas,0,0,this.width,this.height),a.axisY.maskCanvas&&c.drawImage(a.axisY.maskCanvas,0,0,this.width,this.height),this._breaksCanvasCtx&&this._breaksCanvasCtx.drawImage(this._preRenderCanvas,0,0,this.width,this.height),c.clearRect(w.x1,w.y1,w.width,w.height), +this._eventManager.ghostCtx.restore());c.restore();return{source:b,dest:this.plotArea.ctx,animationCallback:M.xClipAnimation,easingFunction:M.easing.linear,animationBase:0}}};p.prototype.renderStepArea=function(a){var d,b;function c(){A&&(0=a.axisY.viewportMinimum&&0<=a.axisY.viewportMaximum?y=v:0>a.axisY.viewportMaximum?y=w.y1:0=a.dataSeriesIndexes.length)){var k=this._eventManager.ghostCtx,l=a.axisX.lineCoordinates,w=a.axisY.lineCoordinates,m=[],s=this.plotArea,q;g.save();r&&k.save();g.beginPath();g.rect(s.x1,s.y1,s.width,s.height);g.clip();r&&(k.beginPath(),k.rect(s.x1,s.y1,s.width,s.height),k.clip());for(var n=0;na.axisX.dataInfo.viewPortMax&&(!B.connectNullData||!b))){var Z=u;"number"!==typeof h[p].y?(B.connectNullData||(b||d)||c(),b=!0):(t=a.axisX.convertValueToPixel(x),u=a.axisY.convertValueToPixel(h[p].y),d||b?(!d&&B.connectNullData?(g.setLineDash&&(B.options.nullDataLineDashType||Y===B.lineDashType&&B.lineDashType!==B.nullDataLineDashType)&&(d= +t,b=u,t=q.x,u=q.y,c(),g.moveTo(q.x,q.y),t=d,u=b,A=q,Y=B.nullDataLineDashType,g.setLineDash(ca)),g.lineTo(t,Z),g.lineTo(t,u),r&&(k.lineTo(t,Z),k.lineTo(t,u))):(g.beginPath(),g.moveTo(t,u),r&&(k.beginPath(),k.moveTo(t,u)),A={x:t,y:u}),b=d=!1):(g.lineTo(t,Z),r&&k.lineTo(t,Z),g.lineTo(t,u),r&&k.lineTo(t,u),0==p%250&&c()),q={x:t,y:u},ph[p].y===a.axisY.reversed?1:-1,color:z}))}c();ia.drawMarkers(m)}}r&&(e.drawImage(this._preRenderCanvas,0,0,this.width,this.height),g.globalCompositeOperation="source-atop",a.axisX.maskCanvas&&g.drawImage(a.axisX.maskCanvas,0,0,this.width,this.height),a.axisY.maskCanvas&&g.drawImage(a.axisY.maskCanvas,0,0,this.width,this.height),this._breaksCanvasCtx&&this._breaksCanvasCtx.drawImage(this._preRenderCanvas, +0,0,this.width,this.height),g.clearRect(s.x1,s.y1,s.width,s.height),this._eventManager.ghostCtx.restore());g.restore();return{source:e,dest:this.plotArea.ctx,animationCallback:M.xClipAnimation,easingFunction:M.easing.linear,animationBase:0}}};p.prototype.renderStackedArea=function(a){function d(){if(!(1>m.length)){for(0=a.dataSeriesIndexes.length)){var e=null,g=null,k=[],l=this.plotArea,w=[],m=[],s=[],q=[],n=0,f,h,p=a.axisY.convertValueToPixel(a.axisY.logarithmic?a.axisY.viewportMinimum:0),u=this._eventManager.ghostCtx,t,C,x;r&&u.beginPath();c.save();r&&u.save();c.beginPath();c.rect(l.x1,l.y1,l.width,l.height);c.clip();r&&(u.beginPath(),u.rect(l.x1,l.y1,l.width,l.height),u.clip());for(var e=[],v=0;va.axisX.dataInfo.viewPortMax&&(!A.connectNullData||!da)))if("number"!==typeof Z.y)A.connectNullData||(da||C)||d(),da=!0;else{f=a.axisX.convertValueToPixel(g);var oa= +w[g]?w[g]:0;if(a.axisY.logarithmic||a.axisY.scaleBreaks&&0=q[g]&&a.axisY.logarithmic)continue;h=a.axisY.convertValueToPixel(q[g])}else h=a.axisY.convertValueToPixel(Z.y),h-=oa;m.push({x:f,y:p-oa});w[g]=p-h;C||da?(!C&&A.connectNullData?(c.setLineDash&&(A.options.nullDataLineDashType||x===A.lineDashType&&A.lineDashType!==A.nullDataLineDashType)&&(C=m.pop(),x=m[m.length-1],d(),c.moveTo(t.x,t.y),m.push(x),m.push(C),x=A.nullDataLineDashType, +c.setLineDash(Y)),c.lineTo(f,h),r&&u.lineTo(f,h)):(c.beginPath(),c.moveTo(f,h),r&&(u.beginPath(),u.moveTo(f,h))),da=C=!1):(c.lineTo(f,h),r&&u.lineTo(f,h),0==n%250&&(d(),c.moveTo(f,h),r&&u.moveTo(f,h),m.push({x:f,y:p-oa})));t={x:f,y:h};nz[n].y===a.axisY.reversed?1:-1,color:e})}}d();c.moveTo(f,h);r&&u.moveTo(f,h)}delete A.dataPointIndexes}ia.drawMarkers(k);r&&(b.drawImage(this._preRenderCanvas,0,0,this.width,this.height),c.globalCompositeOperation="source-atop",a.axisX.maskCanvas&&c.drawImage(a.axisX.maskCanvas,0,0,this.width,this.height),a.axisY.maskCanvas&& +c.drawImage(a.axisY.maskCanvas,0,0,this.width,this.height),this._breaksCanvasCtx&&this._breaksCanvasCtx.drawImage(this._preRenderCanvas,0,0,this.width,this.height),c.clearRect(l.x1,l.y1,l.width,l.height),u.restore());c.restore();return{source:b,dest:this.plotArea.ctx,animationCallback:M.xClipAnimation,easingFunction:M.easing.linear,animationBase:0}}};p.prototype.renderStackedArea100=function(a){function d(){for(0=a.dataSeriesIndexes.length)){var e=null,g=null,k=this.plotArea,l=[],w=[],m=[],s=[],q=[],n=0,f,h,p,u,t,C=a.axisY.convertValueToPixel(a.axisY.logarithmic?a.axisY.viewportMinimum:0),x=this._eventManager.ghostCtx;c.save();r&&x.save();c.beginPath();c.rect(k.x1,k.y1,k.width,k.height);c.clip();r&&(x.beginPath(), +x.rect(k.x1,k.y1,k.width,k.height),x.clip());for(var e=[],v=0;va.axisX.dataInfo.viewPortMax&&(!A.connectNullData|| +!da)))if("number"!==typeof Z.y)A.connectNullData||(da||u)||d(),da=!0;else{var oa;oa=0!==a.dataPointYSums[g]?100*(Z.y/a.dataPointYSums[g]):0;f=a.axisX.convertValueToPixel(g);var la=w[g]?w[g]:0;if(a.axisY.logarithmic||a.axisY.scaleBreaks&&0=q[g]&&a.axisY.logarithmic)continue;h=a.axisY.convertValueToPixel(q[g])}else h=a.axisY.convertValueToPixel(oa),h-=la;m.push({x:f,y:C-la});w[g]=C-h;u||da?(!u&&A.connectNullData?(c.setLineDash&& +(A.options.nullDataLineDashType||t===A.lineDashType&&A.lineDashType!==A.nullDataLineDashType)&&(u=m.pop(),t=m[m.length-1],d(),c.moveTo(p.x,p.y),m.push(t),m.push(u),t=A.nullDataLineDashType,c.setLineDash(Y)),c.lineTo(f,h),r&&x.lineTo(f,h)):(c.beginPath(),c.moveTo(f,h),r&&(x.beginPath(),x.moveTo(f,h))),da=u=!1):(c.lineTo(f,h),r&&x.lineTo(f,h),0==n%250&&(d(),c.moveTo(f,h),r&&x.moveTo(f,h),m.push({x:f,y:C-la})));p={x:f,y:h};nz[n].y===a.axisY.reversed?1:-1,color:e})}}d();c.moveTo(f,h);r&&x.moveTo(f,h)}delete A.dataPointIndexes}ia.drawMarkers(l);r&&(b.drawImage(this._preRenderCanvas,0, +0,this.width,this.height),c.globalCompositeOperation="source-atop",a.axisX.maskCanvas&&c.drawImage(a.axisX.maskCanvas,0,0,this.width,this.height),a.axisY.maskCanvas&&c.drawImage(a.axisY.maskCanvas,0,0,this.width,this.height),this._breaksCanvasCtx&&this._breaksCanvasCtx.drawImage(this._preRenderCanvas,0,0,this.width,this.height),c.clearRect(k.x1,k.y1,k.width,k.height),x.restore());c.restore();return{source:b,dest:this.plotArea.ctx,animationCallback:M.xClipAnimation,easingFunction:M.easing.linear,animationBase:0}}}; +p.prototype.renderBubble=function(a){var d=a.targetCanvasCtx||this.plotArea.ctx,b=r?this._preRenderCtx:d;if(!(0>=a.dataSeriesIndexes.length)){var c=this.plotArea,e=0,g,k;b.save();r&&this._eventManager.ghostCtx.save();b.beginPath();b.rect(c.x1,c.y1,c.width,c.height);b.clip();r&&(this._eventManager.ghostCtx.beginPath(),this._eventManager.ghostCtx.rect(c.x1,c.y1,c.width,c.height),this._eventManager.ghostCtx.clip());for(var l=-Infinity,w=Infinity,m=0;ma.axisX.dataInfo.viewPortMax||"undefined"===typeof n[e].z||(f=n[e].z,f>l&&(l=f),fa.axisX.dataInfo.viewPortMax)&&"number"===typeof n[e].y){g=a.axisX.convertValueToPixel(g);k=a.axisY.convertValueToPixel(n[e].y);var f=n[e].z,u=2*Math.max(Math.sqrt((l===w?p/2:h+(p-h)/(l-w)*(f-w))/Math.PI)<<0,1),f=q.getMarkerProperties(e,b);f.size=u;b.globalAlpha=q.fillOpacity;ia.drawMarker(g,k,b,f.type,f.size,f.color,f.borderColor,f.borderThickness);b.globalAlpha=1;var t=q.dataPointIds[e];this._eventManager.objectMap[t]={id:t,objectType:"dataPoint",dataSeriesIndex:s, +dataPointIndex:e,x1:g,y1:k,size:u};u=N(t);r&&ia.drawMarker(g,k,this._eventManager.ghostCtx,f.type,f.size,u,u,f.borderThickness);(n[e].indexLabel||q.indexLabel||n[e].indexLabelFormatter||q.indexLabelFormatter)&&this._indexLabels.push({chartType:"bubble",dataPoint:n[e],dataSeries:q,point:{x:g,y:k},direction:1,bounds:{x1:g-f.size/2,y1:k-f.size/2,x2:g+f.size/2,y2:k+f.size/2},color:null})}r&&(d.drawImage(this._preRenderCanvas,0,0,this.width,this.height),b.globalCompositeOperation="source-atop",a.axisX.maskCanvas&& +b.drawImage(a.axisX.maskCanvas,0,0,this.width,this.height),a.axisY.maskCanvas&&b.drawImage(a.axisY.maskCanvas,0,0,this.width,this.height),this._breaksCanvasCtx&&this._breaksCanvasCtx.drawImage(this._preRenderCanvas,0,0,this.width,this.height),b.clearRect(c.x1,c.y1,c.width,c.height),this._eventManager.ghostCtx.restore());b.restore();return{source:d,dest:this.plotArea.ctx,animationCallback:M.fadeInAnimation,easingFunction:M.easing.easeInQuad,animationBase:0}}};p.prototype.renderScatter=function(a){var d= +a.targetCanvasCtx||this.plotArea.ctx,b=r?this._preRenderCtx:d;if(!(0>=a.dataSeriesIndexes.length)){var c=this.plotArea,e=0,g,k;b.save();r&&this._eventManager.ghostCtx.save();b.beginPath();b.rect(c.x1,c.y1,c.width,c.height);b.clip();r&&(this._eventManager.ghostCtx.beginPath(),this._eventManager.ghostCtx.rect(c.x1,c.y1,c.width,c.height),this._eventManager.ghostCtx.clip());for(var l=0;la.axisX.dataInfo.viewPortMax)&&"number"===typeof s[e].y){g=a.axisX.convertValueToPixel(g);k=a.axisY.convertValueToPixel(s[e].y);var f=m.getMarkerProperties(e,g,k,b);b.globalAlpha=m.fillOpacity;ia.drawMarker(f.x,f.y,f.ctx,f.type,f.size,f.color,f.borderColor,f.borderThickness);b.globalAlpha=1;Math.sqrt((q-g)*(q-g)+(n-k)*(n-k))Math.min(this.plotArea.width,this.plotArea.height)||(q=m.dataPointIds[e],this._eventManager.objectMap[q]={id:q,objectType:"dataPoint",dataSeriesIndex:w,dataPointIndex:e,x1:g,y1:k},q=N(q),r&&ia.drawMarker(f.x,f.y,this._eventManager.ghostCtx,f.type,f.size,q,q,f.borderThickness),(s[e].indexLabel||m.indexLabel||s[e].indexLabelFormatter||m.indexLabelFormatter)&&this._indexLabels.push({chartType:"scatter",dataPoint:s[e],dataSeries:m,point:{x:g,y:k},direction:1,bounds:{x1:g-f.size/2,y1:k-f.size/ +2,x2:g+f.size/2,y2:k+f.size/2},color:null}),q=g,n=k)}}}r&&(d.drawImage(this._preRenderCanvas,0,0,this.width,this.height),b.globalCompositeOperation="source-atop",a.axisX.maskCanvas&&b.drawImage(a.axisX.maskCanvas,0,0,this.width,this.height),a.axisY.maskCanvas&&b.drawImage(a.axisY.maskCanvas,0,0,this.width,this.height),this._breaksCanvasCtx&&this._breaksCanvasCtx.drawImage(this._preRenderCanvas,0,0,this.width,this.height),b.clearRect(c.x1,c.y1,c.width,c.height),this._eventManager.ghostCtx.restore()); +b.restore();return{source:d,dest:this.plotArea.ctx,animationCallback:M.fadeInAnimation,easingFunction:M.easing.easeInQuad,animationBase:0}}};p.prototype.renderCandlestick=function(a){var d=a.targetCanvasCtx||this.plotArea.ctx,b=r?this._preRenderCtx:d,c=this._eventManager.ghostCtx;if(!(0>=a.dataSeriesIndexes.length)){var e=null,g=null,k=this.plotArea,l=0,w,m,s,q,n,f,e=this.options.dataPointMinWidth?this.dataPointMinWidth:this.options.dataPointWidth?this.dataPointWidth:1,g=this.options.dataPointMaxWidth? +this.dataPointMaxWidth:this.options.dataPointWidth?this.dataPointWidth:0.015*this.width,h=a.axisX.dataInfo.minDiff;isFinite(h)||(h=0.3*Math.abs(a.axisX.range));h=this.options.dataPointWidth?this.dataPointWidth:0.7*k.width*(a.axisX.logarithmic?Math.log(h)/Math.log(a.axisX.range):Math.abs(h)/Math.abs(a.axisX.range))<<0;this.dataPointMaxWidth&&e>g&&(e=Math.min(this.options.dataPointWidth?this.dataPointWidth:Infinity,g));!this.dataPointMaxWidth&&(this.dataPointMinWidth&&gg&&(h=g);b.save();r&&c.save();b.beginPath();b.rect(k.x1,k.y1,k.width,k.height);b.clip();r&&(c.beginPath(),c.rect(k.x1,k.y1,k.width,k.height),c.clip());for(var p=0;pa.axisX.dataInfo.viewPortMax)&&!u(C[l].y)&&C[l].y.length&& +"number"===typeof C[l].y[0]&&"number"===typeof C[l].y[1]&&"number"===typeof C[l].y[2]&&"number"===typeof C[l].y[3]){w=a.axisX.convertValueToPixel(f);m=a.axisY.convertValueToPixel(C[l].y[0]);s=a.axisY.convertValueToPixel(C[l].y[1]);q=a.axisY.convertValueToPixel(C[l].y[2]);n=a.axisY.convertValueToPixel(C[l].y[3]);var z=w-h/2<<0,y=z+h<<0,g=t.options.fallingColor?t.fallingColor:t._colorSet[0],e=C[l].color?C[l].color:t._colorSet[0],A=Math.round(Math.max(1,0.15*h)),D=0===A%2?0:0.5,aa=t.dataPointIds[l]; +this._eventManager.objectMap[aa]={id:aa,objectType:"dataPoint",dataSeriesIndex:v,dataPointIndex:l,x1:z,y1:m,x2:y,y2:s,x3:w,y3:q,x4:w,y4:n,borderThickness:A,color:e};b.strokeStyle=e;b.beginPath();b.lineWidth=A;c.lineWidth=Math.max(A,4);"candlestick"===t.type?(b.moveTo(w-D,s),b.lineTo(w-D,Math.min(m,n)),b.stroke(),b.moveTo(w-D,Math.max(m,n)),b.lineTo(w-D,q),b.stroke(),ea(b,z,Math.min(m,n),y,Math.max(m,n),C[l].y[0]<=C[l].y[3]?t.risingColor:g,A,e,x,x,!1,!1,t.fillOpacity),r&&(e=N(aa),c.strokeStyle=e,c.moveTo(w- +D,s),c.lineTo(w-D,Math.min(m,n)),c.stroke(),c.moveTo(w-D,Math.max(m,n)),c.lineTo(w-D,q),c.stroke(),ea(c,z,Math.min(m,n),y,Math.max(m,n),e,0,null,!1,!1,!1,!1))):"ohlc"===t.type&&(b.moveTo(w-D,s),b.lineTo(w-D,q),b.stroke(),b.beginPath(),b.moveTo(w,m),b.lineTo(z,m),b.stroke(),b.beginPath(),b.moveTo(w,n),b.lineTo(y,n),b.stroke(),r&&(e=N(aa),c.strokeStyle=e,c.moveTo(w-D,s),c.lineTo(w-D,q),c.stroke(),c.beginPath(),c.moveTo(w,m),c.lineTo(z,m),c.stroke(),c.beginPath(),c.moveTo(w,n),c.lineTo(y,n),c.stroke())); +(C[l].indexLabel||t.indexLabel||C[l].indexLabelFormatter||t.indexLabelFormatter)&&this._indexLabels.push({chartType:t.type,dataPoint:C[l],dataSeries:t,point:{x:z+(y-z)/2,y:a.axisY.reversed?q:s},direction:1,bounds:{x1:z,y1:Math.min(s,q),x2:y,y2:Math.max(s,q)},color:e})}}r&&(d.drawImage(this._preRenderCanvas,0,0,this.width,this.height),b.globalCompositeOperation="source-atop",a.axisX.maskCanvas&&b.drawImage(a.axisX.maskCanvas,0,0,this.width,this.height),a.axisY.maskCanvas&&b.drawImage(a.axisY.maskCanvas, +0,0,this.width,this.height),this._breaksCanvasCtx&&this._breaksCanvasCtx.drawImage(this._preRenderCanvas,0,0,this.width,this.height),b.clearRect(k.x1,k.y1,k.width,k.height),c.restore());b.restore();return{source:d,dest:this.plotArea.ctx,animationCallback:M.fadeInAnimation,easingFunction:M.easing.easeInQuad,animationBase:0}}};p.prototype.renderBoxAndWhisker=function(a){var d=a.targetCanvasCtx||this.plotArea.ctx,b=r?this._preRenderCtx:d,c=this._eventManager.ghostCtx;if(!(0>=a.dataSeriesIndexes.length)){var e= +null,g=this.plotArea,k=0,l,w,m,s,q,n,f,e=this.options.dataPointMinWidth?this.dataPointMinWidth:this.options.dataPointWidth?this.dataPointWidth:1,k=this.options.dataPointMaxWidth?this.dataPointMaxWidth:this.options.dataPointWidth?this.dataPointWidth:0.015*this.width,h=a.axisX.dataInfo.minDiff;isFinite(h)||(h=0.3*Math.abs(a.axisX.range));h=this.options.dataPointWidth?this.dataPointWidth:0.7*g.width*(a.axisX.logarithmic?Math.log(h)/Math.log(a.axisX.range):Math.abs(h)/Math.abs(a.axisX.range))<<0;this.dataPointMaxWidth&& +e>k&&(e=Math.min(this.options.dataPointWidth?this.dataPointWidth:Infinity,k));!this.dataPointMaxWidth&&(this.dataPointMinWidth&&kk&&(h=k);b.save();r&&c.save();b.beginPath();b.rect(g.x1,g.y1,g.width,g.height);b.clip();r&&(c.beginPath(),c.rect(g.x1,g.y1,g.width,g.height),c.clip());for(var p=!1,p=!!a.axisY.reversed,v=0;va.axisX.dataInfo.viewPortMax)&&!u(x[k].y)&&x[k].y.length&&"number"===typeof x[k].y[0]&&"number"===typeof x[k].y[1]&&"number"===typeof x[k].y[2]&&"number"===typeof x[k].y[3]&&"number"===typeof x[k].y[4]&&5===x[k].y.length){l=a.axisX.convertValueToPixel(f);w=a.axisY.convertValueToPixel(x[k].y[0]);m=a.axisY.convertValueToPixel(x[k].y[1]);s=a.axisY.convertValueToPixel(x[k].y[2]); +q=a.axisY.convertValueToPixel(x[k].y[3]);n=a.axisY.convertValueToPixel(x[k].y[4]);var y=l-h/2<<0,A=l+h/2<<0,e=x[k].color?x[k].color:C._colorSet[0],D=Math.round(Math.max(1,0.15*h)),aa=0===D%2?0:0.5,T=x[k].whiskerColor?x[k].whiskerColor:x[k].color?C.whiskerColor?C.whiskerColor:x[k].color:C.whiskerColor?C.whiskerColor:e,Y="number"===typeof x[k].whiskerThickness?x[k].whiskerThickness:"number"===typeof C.options.whiskerThickness?C.whiskerThickness:D,ca=x[k].whiskerDashType?x[k].whiskerDashType:C.whiskerDashType, +da=u(x[k].whiskerLength)?u(C.options.whiskerLength)?h:C.whiskerLength:x[k].whiskerLength,da="number"===typeof da?0>=da?0:da>=h?h:da:"string"===typeof da?parseInt(da)*h/100>h?h:parseInt(da)*h/100:h,Z=1===Math.round(Y)%2?0.5:0,oa=x[k].stemColor?x[k].stemColor:x[k].color?C.stemColor?C.stemColor:x[k].color:C.stemColor?C.stemColor:e,la="number"===typeof x[k].stemThickness?x[k].stemThickness:"number"===typeof C.options.stemThickness?C.stemThickness:D,G=1===Math.round(la)%2?0.5:0,F=x[k].stemDashType?x[k].stemDashType: +C.stemDashType,E=x[k].lineColor?x[k].lineColor:x[k].color?C.lineColor?C.lineColor:x[k].color:C.lineColor?C.lineColor:e,H="number"===typeof x[k].lineThickness?x[k].lineThickness:"number"===typeof C.options.lineThickness?C.lineThickness:D,I=x[k].lineDashType?x[k].lineDashType:C.lineDashType,K=1===Math.round(H)%2?0.5:0,L=C.upperBoxColor,O=C.lowerBoxColor,Q=u(C.options.fillOpacity)?1:C.fillOpacity,P=C.dataPointIds[k];this._eventManager.objectMap[P]={id:P,objectType:"dataPoint",dataSeriesIndex:t,dataPointIndex:k, +x1:y,y1:w,x2:A,y2:m,x3:l,y3:s,x4:l,y4:q,y5:n,borderThickness:D,color:e,stemThickness:la,stemColor:oa,whiskerThickness:Y,whiskerLength:da,whiskerColor:T,lineThickness:H,lineColor:E};b.save();0=a.dataSeriesIndexes.length)){var c=null,e=this.plotArea,g=0,k,l,w,g=this.options.dataPointMinWidth?this.dataPointMinWidth:this.options.dataPointWidth?this.dataPointWidth: +1;k=this.options.dataPointMaxWidth?this.dataPointMaxWidth:this.options.dataPointWidth?this.dataPointWidth:0.03*this.width;var m=a.axisX.dataInfo.minDiff;isFinite(m)||(m=0.3*Math.abs(a.axisX.range));m=this.options.dataPointWidth?this.dataPointWidth:0.9*(e.width*(a.axisX.logarithmic?Math.log(m)/Math.log(a.axisX.range):Math.abs(m)/Math.abs(a.axisX.range))/a.plotType.totalDataSeries)<<0;this.dataPointMaxWidth&&g>k&&(g=Math.min(this.options.dataPointWidth?this.dataPointWidth:Infinity,k));!this.dataPointMaxWidth&& +(this.dataPointMinWidth&&kk&&(m=k);b.save();r&&this._eventManager.ghostCtx.save();b.beginPath();b.rect(e.x1,e.y1,e.width,e.height);b.clip();r&&(this._eventManager.ghostCtx.beginPath(),this._eventManager.ghostCtx.rect(e.x1,e.y1,e.width,e.height),this._eventManager.ghostCtx.clip());for(var s=0;sa.axisX.dataInfo.viewPortMax)&&!u(f[g].y)&&f[g].y.length&&"number"===typeof f[g].y[0]&&"number"===typeof f[g].y[1]){c=a.axisX.convertValueToPixel(w);k=a.axisY.convertValueToPixel(f[g].y[0]);l=a.axisY.convertValueToPixel(f[g].y[1]);var p=a.axisX.reversed?c+a.plotType.totalDataSeries*m/2-(a.previousDataSeriesCount+s)*m<<0:c-a.plotType.totalDataSeries*m/2+(a.previousDataSeriesCount+ +s)*m<<0,v=a.axisX.reversed?p-m<<0:p+m<<0,c=f[g].color?f[g].color:n._colorSet[g%n._colorSet.length];if(k>l){var t=k;k=l;l=t}t=n.dataPointIds[g];this._eventManager.objectMap[t]={id:t,objectType:"dataPoint",dataSeriesIndex:q,dataPointIndex:g,x1:p,y1:k,x2:v,y2:l};ea(b,p,k,v,l,c,0,c,h,h,!1,!1,n.fillOpacity);c=N(t);r&&ea(this._eventManager.ghostCtx,p,k,v,l,c,0,null,!1,!1,!1,!1);if(f[g].indexLabel||n.indexLabel||f[g].indexLabelFormatter||n.indexLabelFormatter)this._indexLabels.push({chartType:"rangeColumn", +dataPoint:f[g],dataSeries:n,indexKeyword:0,point:{x:p+(v-p)/2,y:f[g].y[1]>=f[g].y[0]?l:k},direction:f[g].y[1]>=f[g].y[0]?-1:1,bounds:{x1:p,y1:Math.min(k,l),x2:v,y2:Math.max(k,l)},color:c}),this._indexLabels.push({chartType:"rangeColumn",dataPoint:f[g],dataSeries:n,indexKeyword:1,point:{x:p+(v-p)/2,y:f[g].y[1]>=f[g].y[0]?k:l},direction:f[g].y[1]>=f[g].y[0]?1:-1,bounds:{x1:p,y1:Math.min(k,l),x2:v,y2:Math.max(k,l)},color:c})}}r&&(d.drawImage(this._preRenderCanvas,0,0,this.width,this.height),b.globalCompositeOperation= +"source-atop",a.axisX.maskCanvas&&b.drawImage(a.axisX.maskCanvas,0,0,this.width,this.height),a.axisY.maskCanvas&&b.drawImage(a.axisY.maskCanvas,0,0,this.width,this.height),this._breaksCanvasCtx&&this._breaksCanvasCtx.drawImage(this._preRenderCanvas,0,0,this.width,this.height),b.clearRect(e.x1,e.y1,e.width,e.height),this._eventManager.ghostCtx.restore());b.restore();return{source:d,dest:this.plotArea.ctx,animationCallback:M.fadeInAnimation,easingFunction:M.easing.easeInQuad,animationBase:0}}};p.prototype.renderError= +function(a){var d=a.targetCanvasCtx||this.plotArea.ctx,b=r?this._preRenderCtx:d,c=a.axisY._position?"left"===a.axisY._position||"right"===a.axisY._position?!1:!0:!1;if(!(0>=a.dataSeriesIndexes.length)){var e=null,g=!1,k=this.plotArea,l=0,w,m,s,q,n,f,h,p=a.axisX.dataInfo.minDiff;isFinite(p)||(p=0.3*Math.abs(a.axisX.range));b.save();r&&this._eventManager.ghostCtx.save();b.beginPath();b.rect(k.x1,k.y1,k.width,k.height);b.clip();r&&(this._eventManager.ghostCtx.beginPath(),this._eventManager.ghostCtx.rect(k.x1, +k.y1,k.width,k.height),this._eventManager.ghostCtx.clip());for(var v=0,t=0;tl&&(e=Math.min(this.options.dataPointWidth?this.dataPointWidth:Infinity,l));!this.dataPointMaxWidth&&(this.dataPointMinWidth&&ll&&(t=l);if(0=T.length?0:T.length>=t?t:T.length:"string"===typeof T.length?parseInt(T.length)*t/100>t?t:parseInt(T.length)*t/100>t:t;T.thickness="number"===typeof T.thickness?0>T.thickness?0:Math.round(T.thickness):2;var Y={color:y[l].stemColor?y[l].stemColor:y[l].color?z.stemColor?z.stemColor:y[l].color:z.stemColor?z.stemColor:e,thickness:y[l].stemThickness?y[l].stemThickness:z.stemThickness,dashType:y[l].stemDashType? +y[l].stemDashType:z.stemDashType};Y.thickness="number"===typeof Y.thickness?0>Y.thickness?0:Math.round(Y.thickness):2;y[l].getTime?h=y[l].x.getTime():h=y[l].x;if(!(ha.axisX.dataInfo.viewPortMax)&&!u(y[l].y)&&y[l].y.length&&"number"===typeof y[l].y[0]&&"number"===typeof y[l].y[1]){var ca=a.axisX.convertValueToPixel(h);c?m=ca:w=ca;ca=a.axisY.convertValueToPixel(y[l].y[0]);c?s=ca:n=ca;ca=a.axisY.convertValueToPixel(y[l].y[1]);c?q=ca:f=ca;c?(n=a.axisX.reversed?m+(A?v: +1)*t/2-(A?D-1:0)*t<<0:m-(A?v:1)*t/2+(A?D-1:0)*t<<0,f=a.axisX.reversed?n-t<<0:n+t<<0):(s=a.axisX.reversed?w+(A?v:1)*t/2-(A?D-1:0)*t<<0:w-(A?v:1)*t/2+(A?D-1:0)*t<<0,q=a.axisX.reversed?s-t<<0:s+t<<0);!c&&n>f&&(ca=n,n=f,f=ca);c&&s>q&&(ca=s,s=q,q=ca);ca=z.dataPointIds[l];this._eventManager.objectMap[ca]={id:ca,objectType:"dataPoint",dataSeriesIndex:x,dataPointIndex:l,x1:Math.min(s,q),y1:Math.min(n,f),x2:Math.max(q,s),y2:Math.max(f,n),isXYSwapped:c,stemProperties:Y,whiskerProperties:T};E(b,Math.min(s,q), +Math.min(n,f),Math.max(q,s),Math.max(f,n),e,T,Y,c);r&&E(this._eventManager.ghostCtx,s,n,q,f,e,T,Y,c);if(y[l].indexLabel||z.indexLabel||y[l].indexLabelFormatter||z.indexLabelFormatter)this._indexLabels.push({chartType:"error",dataPoint:y[l],dataSeries:z,indexKeyword:0,point:{x:c?y[l].y[1]>=y[l].y[0]?s:q:s+(q-s)/2,y:c?n+(f-n)/2:y[l].y[1]>=y[l].y[0]?f:n},direction:y[l].y[1]>=y[l].y[0]?-1:1,bounds:{x1:c?Math.min(s,q):s,y1:c?n:Math.min(n,f),x2:c?Math.max(s,q):q,y2:c?f:Math.max(n,f)},color:e,axisSwapped:c}), +this._indexLabels.push({chartType:"error",dataPoint:y[l],dataSeries:z,indexKeyword:1,point:{x:c?y[l].y[1]>=y[l].y[0]?q:s:s+(q-s)/2,y:c?n+(f-n)/2:y[l].y[1]>=y[l].y[0]?n:f},direction:y[l].y[1]>=y[l].y[0]?1:-1,bounds:{x1:c?Math.min(s,q):s,y1:c?n:Math.min(n,f),x2:c?Math.max(s,q):q,y2:c?f:Math.max(n,f)},color:e,axisSwapped:c})}}}r&&(d.drawImage(this._preRenderCanvas,0,0,this.width,this.height),b.globalCompositeOperation="source-atop",a.axisX.maskCanvas&&b.drawImage(a.axisX.maskCanvas,0,0,this.width,this.height), +a.axisY.maskCanvas&&b.drawImage(a.axisY.maskCanvas,0,0,this.width,this.height),this._breaksCanvasCtx&&this._breaksCanvasCtx.drawImage(this._preRenderCanvas,0,0,this.width,this.height),b.clearRect(k.x1,k.y1,k.width,k.height),this._eventManager.ghostCtx.restore());b.restore();return{source:d,dest:this.plotArea.ctx,animationCallback:M.fadeInAnimation,easingFunction:M.easing.easeInQuad,animationBase:0}}};p.prototype.renderRangeBar=function(a){var d=a.targetCanvasCtx||this.plotArea.ctx,b=r?this._preRenderCtx: +d;if(!(0>=a.dataSeriesIndexes.length)){var c=null,e=this.plotArea,g=0,k,l,w,m,g=this.options.dataPointMinWidth?this.dataPointMinWidth:this.options.dataPointWidth?this.dataPointWidth:1;k=this.options.dataPointMaxWidth?this.dataPointMaxWidth:this.options.dataPointWidth?this.dataPointWidth:Math.min(0.15*this.height,0.9*(this.plotArea.height/a.plotType.totalDataSeries))<<0;var s=a.axisX.dataInfo.minDiff;isFinite(s)||(s=0.3*Math.abs(a.axisX.range));s=this.options.dataPointWidth?this.dataPointWidth:0.9* +(e.height*(a.axisX.logarithmic?Math.log(s)/Math.log(a.axisX.range):Math.abs(s)/Math.abs(a.axisX.range))/a.plotType.totalDataSeries)<<0;this.dataPointMaxWidth&&g>k&&(g=Math.min(this.options.dataPointWidth?this.dataPointWidth:Infinity,k));!this.dataPointMaxWidth&&(this.dataPointMinWidth&&kk&&(s=k);b.save();r&&this._eventManager.ghostCtx.save();b.beginPath();b.rect(e.x1,e.y1,e.width,e.height);b.clip();r&&(this._eventManager.ghostCtx.beginPath(), +this._eventManager.ghostCtx.rect(e.x1,e.y1,e.width,e.height),this._eventManager.ghostCtx.clip());for(var q=0;qa.axisX.dataInfo.viewPortMax)&&!u(h[g].y)&&h[g].y.length&&"number"===typeof h[g].y[0]&&"number"===typeof h[g].y[1]){k=a.axisY.convertValueToPixel(h[g].y[0]); +l=a.axisY.convertValueToPixel(h[g].y[1]);w=a.axisX.convertValueToPixel(m);w=a.axisX.reversed?w+a.plotType.totalDataSeries*s/2-(a.previousDataSeriesCount+q)*s<<0:w-a.plotType.totalDataSeries*s/2+(a.previousDataSeriesCount+q)*s<<0;var v=a.axisX.reversed?w-s<<0:w+s<<0;k>l&&(c=k,k=l,l=c);c=h[g].color?h[g].color:f._colorSet[g%f._colorSet.length];ea(b,k,w,l,v,c,0,null,p,!1,!1,!1,f.fillOpacity);c=f.dataPointIds[g];this._eventManager.objectMap[c]={id:c,objectType:"dataPoint",dataSeriesIndex:n,dataPointIndex:g, +x1:k,y1:w,x2:l,y2:v};c=N(c);r&&ea(this._eventManager.ghostCtx,k,w,l,v,c,0,null,!1,!1,!1,!1);if(h[g].indexLabel||f.indexLabel||h[g].indexLabelFormatter||f.indexLabelFormatter)this._indexLabels.push({chartType:"rangeBar",dataPoint:h[g],dataSeries:f,indexKeyword:0,point:{x:h[g].y[1]>=h[g].y[0]?k:l,y:w+(v-w)/2},direction:h[g].y[1]>=h[g].y[0]?-1:1,bounds:{x1:Math.min(k,l),y1:w,x2:Math.max(k,l),y2:v},color:c}),this._indexLabels.push({chartType:"rangeBar",dataPoint:h[g],dataSeries:f,indexKeyword:1,point:{x:h[g].y[1]>= +h[g].y[0]?l:k,y:w+(v-w)/2},direction:h[g].y[1]>=h[g].y[0]?1:-1,bounds:{x1:Math.min(k,l),y1:w,x2:Math.max(k,l),y2:v},color:c})}}}r&&(d.drawImage(this._preRenderCanvas,0,0,this.width,this.height),b.globalCompositeOperation="source-atop",a.axisX.maskCanvas&&b.drawImage(a.axisX.maskCanvas,0,0,this.width,this.height),a.axisY.maskCanvas&&b.drawImage(a.axisY.maskCanvas,0,0,this.width,this.height),this._breaksCanvasCtx&&this._breaksCanvasCtx.drawImage(this._preRenderCanvas,0,0,this.width,this.height),b.clearRect(e.x1, +e.y1,e.width,e.height),this._eventManager.ghostCtx.restore());b.restore();return{source:d,dest:this.plotArea.ctx,animationCallback:M.fadeInAnimation,easingFunction:M.easing.easeInQuad,animationBase:0}}};p.prototype.renderRangeArea=function(a){function d(){if(C){var a=null;0=a.dataSeriesIndexes.length)){var e=this._eventManager.ghostCtx,g=[],k=this.plotArea;c.save();r&&e.save();c.beginPath();c.rect(k.x1,k.y1,k.width,k.height);c.clip();r&&(e.beginPath(),e.rect(k.x1,k.y1,k.width,k.height),e.clip());for(var l=0;la.axisX.dataInfo.viewPortMax&&(!s.connectNullData||!T)))if(null!==q[f].y&&q[f].y.length&&"number"===typeof q[f].y[0]&&"number"===typeof q[f].y[1]){h=a.axisX.convertValueToPixel(t);p=a.axisY.convertValueToPixel(q[f].y[0]);u=a.axisY.convertValueToPixel(q[f].y[1]);n||T?(s.connectNullData&&!n?(c.setLineDash&&(s.options.nullDataLineDashType||A===s.lineDashType&&s.lineDashType!==s.nullDataLineDashType)&&(w[w.length- +1].newLineDashArray=D,A=s.nullDataLineDashType,c.setLineDash(z)),c.lineTo(h,p),r&&e.lineTo(h,p),w.push({x:h,y:u})):(c.beginPath(),c.moveTo(h,p),C={x:h,y:p},w=[],w.push({x:h,y:u}),r&&(e.beginPath(),e.moveTo(h,p))),T=n=!1):(c.lineTo(h,p),w.push({x:h,y:u}),r&&e.lineTo(h,p),0==f%250&&d());t=s.dataPointIds[f];this._eventManager.objectMap[t]={id:t,objectType:"dataPoint",dataSeriesIndex:m,dataPointIndex:f,x1:h,y1:p,y2:u};fq[f].y[1]===a.axisY.reversed?-1:1,color:x}),this._indexLabels.push({chartType:"rangeArea",dataPoint:q[f],dataSeries:s,indexKeyword:1,point:{x:h, +y:u},direction:q[f].y[0]>q[f].y[1]===a.axisY.reversed?1:-1,color:x})}else T||n||d(),T=!0;d();ia.drawMarkers(g)}}r&&(b.drawImage(this._preRenderCanvas,0,0,this.width,this.height),c.globalCompositeOperation="source-atop",a.axisX.maskCanvas&&c.drawImage(a.axisX.maskCanvas,0,0,this.width,this.height),a.axisY.maskCanvas&&c.drawImage(a.axisY.maskCanvas,0,0,this.width,this.height),this._breaksCanvasCtx&&this._breaksCanvasCtx.drawImage(this._preRenderCanvas,0,0,this.width,this.height),c.clearRect(k.x1,k.y1, +k.width,k.height),this._eventManager.ghostCtx.restore());c.restore();return{source:b,dest:this.plotArea.ctx,animationCallback:M.xClipAnimation,easingFunction:M.easing.linear,animationBase:0}}};p.prototype.renderRangeSplineArea=function(a){function d(a,b){var d=v(u,2);if(0=a.dataSeriesIndexes.length)){var e=this._eventManager.ghostCtx,g=[],k=this.plotArea;c.save();r&&e.save();c.beginPath();c.rect(k.x1,k.y1,k.width,k.height);c.clip();r&&(e.beginPath(),e.rect(k.x1,k.y1,k.width, +k.height),e.clip());for(var l=0;la.axisX.dataInfo.viewPortMax&&(!m.connectNullData||!f)))if(null!==h[q].y&&h[q].y.length&&"number"===typeof h[q].y[0]&&"number"===typeof h[q].y[1]){n=a.axisX.convertValueToPixel(n);f=a.axisY.convertValueToPixel(h[q].y[0]);p=a.axisY.convertValueToPixel(h[q].y[1]);var E=m.dataPointIds[q];this._eventManager.objectMap[E]={id:E,objectType:"dataPoint",dataSeriesIndex:w,dataPointIndex:q, +x1:n,y1:f,y2:p};u[u.length]={x:n,y:f};z[z.length]={x:n,y:p};q=a.dataSeriesIndexes.length)){var c=this._eventManager.ghostCtx,e=null,g=this.plotArea,k=0,l,h,m,s,q=a.axisY.convertValueToPixel(a.axisY.logarithmic?a.axisY.viewportMinimum:0),k=this.options.dataPointMinWidth?this.dataPointMinWidth:this.options.dataPointWidth?this.dataPointWidth:1;h=this.options.dataPointMaxWidth?this.dataPointMaxWidth:this.options.dataPointWidth?this.dataPointWidth:Math.min(0.15*this.width,0.9*(this.plotArea.width/a.plotType.totalDataSeries))<<0;var n= +a.axisX.dataInfo.minDiff;isFinite(n)||(n=0.3*Math.abs(a.axisX.range));n=this.options.dataPointWidth?this.dataPointWidth:0.6*(g.width*(a.axisX.logarithmic?Math.log(n)/Math.log(a.axisX.range):Math.abs(n)/Math.abs(a.axisX.range))/a.plotType.totalDataSeries)<<0;this.dataPointMaxWidth&&k>h&&(k=Math.min(this.options.dataPointWidth?this.dataPointWidth:Infinity,h));!this.dataPointMaxWidth&&(this.dataPointMinWidth&&hh&&(n=h);b.save();r&&this._eventManager.ghostCtx.save();b.beginPath();b.rect(g.x1,g.y1,g.width,g.height);b.clip();r&&(this._eventManager.ghostCtx.beginPath(),this._eventManager.ghostCtx.rect(g.x1,g.y1,g.width,g.height),this._eventManager.ghostCtx.clip());for(var f=0;fm&&(e=h,h=m,m=e);a.axisY.reversed&&(e=h,h=m,m=e);e=u.dataPointIds[k];this._eventManager.objectMap[e]={id:e,objectType:"dataPoint",dataSeriesIndex:p,dataPointIndex:k,x1:l,y1:h,x2:F,y2:m}; +var T=v[k].color?v[k].color:0v[k].y===a.axisY.reversed?1:-1,bounds:{x1:l,y1:Math.min(h,m),x2:F,y2:Math.max(h,m)},color:e})}}r&&(d.drawImage(this._preRenderCanvas,0,0,this.width,this.height),b.globalCompositeOperation="source-atop",a.axisX.maskCanvas&&b.drawImage(a.axisX.maskCanvas,0,0,this.width,this.height),a.axisY.maskCanvas&&b.drawImage(a.axisY.maskCanvas,0,0,this.width,this.height),this._breaksCanvasCtx&&this._breaksCanvasCtx.drawImage(this._preRenderCanvas,0,0,this.width,this.height), +b.clearRect(g.x1,g.y1,g.width,g.height),this._eventManager.ghostCtx.restore());b.restore();return{source:d,dest:this.plotArea.ctx,animationCallback:M.fadeInAnimation,easingFunction:M.easing.easeInQuad,animationBase:0}}};var ja=function(a,d,b,c,e,g,k,l,h){if(!(0>b)){"undefined"===typeof l&&(l=1);if(!r){var m=Number((k%(2*Math.PI)).toFixed(8));Number((g%(2*Math.PI)).toFixed(8))===m&&(k-=1E-4)}a.save();a.globalAlpha=l;"pie"===e?(a.beginPath(),a.moveTo(d.x,d.y),a.arc(d.x,d.y,b,g,k,!1),a.fillStyle=c,a.strokeStyle= +"white",a.lineWidth=2,a.closePath(),a.fill()):"doughnut"===e&&(a.beginPath(),a.arc(d.x,d.y,b,g,k,!1),0<=h&&a.arc(d.x,d.y,h*b,k,g,!0),a.closePath(),a.fillStyle=c,a.strokeStyle="white",a.lineWidth=2,a.fill());a.globalAlpha=1;a.restore()}};p.prototype.renderPie=function(a){function d(){if(m&&s){for(var a=0,b=0,c=0,e=0,d=0;dMath.PI/2-t&&k.midAngle +k.midAngle)c=d;a++}else if(k.midAngle>3*Math.PI/2-t&&k.midAngle<3*Math.PI/2+t){if(0===b||f[e].midAngle>k.midAngle)e=d;b++}k.hemisphere=g>Math.PI/2&&g<=3*Math.PI/2?"left":"right";k.indexLabelTextBlock=new ka(h.plotArea.ctx,{fontSize:k.indexLabelFontSize,fontFamily:k.indexLabelFontFamily,fontColor:k.indexLabelFontColor,fontStyle:k.indexLabelFontStyle,fontWeight:k.indexLabelFontWeight,horizontalAlign:"left",backgroundColor:k.indexLabelBackgroundColor,maxWidth:k.indexLabelMaxWidth,maxHeight:k.indexLabelWrap? +5*k.indexLabelFontSize:1.5*k.indexLabelFontSize,text:k.indexLabelText,padding:0,textBaseline:"top"});k.indexLabelTextBlock.measureText()}l=g=0;q=!1;for(d=0;dMath.PI/2-t&&k.midAngle3*Math.PI/2-t&&k.midAngle<3*Math.PI/2+t)&&(l<=b/2&&!q?(k.hemisphere="left",l++):(k.hemisphere="right",q=!0))}}function b(a){var b= +h.plotArea.ctx;b.clearRect(n.x1,n.y1,n.width,n.height);b.fillStyle=h.backgroundColor;b.fillRect(n.x1,n.y1,n.width,n.height);for(b=0;bc){var d=0.07*A*Math.cos(f[b].midAngle),g=0.07*A*Math.sin(f[b].midAngle),k=!1;if(s[b].exploded){if(1E-9a.indexLabelTextBlock.y?d-e:c-f}function e(a){for(var b=null,e=1;ec(f[b],f[a])||("right"===f[a].hemisphere?f[b].indexLabelTextBlock.y>=f[a].indexLabelTextBlock.y:f[b].indexLabelTextBlock.y<=f[a].indexLabelTextBlock.y)))break;else b=null;return b}function g(a,b,d){d=(d||0)+1;if(1E3< +d)return 0;b=b||0;var k=0,m=x.y-1*r,l=x.y+1*r;if(0<=a&&ab&&n.indexLabelTextBlock.yl)return 0;var h=0,q=0,q=h=h=0;0>b?n.indexLabelTextBlock.y-n.indexLabelTextBlock.height/2>m&&n.indexLabelTextBlock.y-n.indexLabelTextBlock.height/2+bl&&(b=n.indexLabelTextBlock.y+ +n.indexLabelTextBlock.height/2+b-l);b=n.indexLabelTextBlock.y+b;m=0;m="right"===n.hemisphere?x.x+Math.sqrt(Math.pow(r,2)-Math.pow(b-x.y,2)):x.x-Math.sqrt(Math.pow(r,2)-Math.pow(b-x.y,2));q=x.x+A*Math.cos(n.midAngle);h=x.y+A*Math.sin(n.midAngle);h=Math.sqrt(Math.pow(m-q,2)+Math.pow(b-h,2));q=Math.acos(A/r);h=Math.acos((r*r+A*A-h*h)/(2*A*r));b=hc(f[m],f[a])||("right"===f[a].hemisphere?f[m].indexLabelTextBlock.y<=f[a].indexLabelTextBlock.y:f[m].indexLabelTextBlock.y>=f[a].indexLabelTextBlock.y)))break;else m=null;q=m;h=e(a);l=m=0;0>b?(l="right"===n.hemisphere?q:h,k=b,null!==l&&(q=-b,b=n.indexLabelTextBlock.y-n.indexLabelTextBlock.height/2-(f[l].indexLabelTextBlock.y+f[l].indexLabelTextBlock.height/2),b-q+m.toFixed(C)&&(k=b>p?-(b-p):-(q-(l-m)))))):0p?b-p:q-(m-l)))));k&&(d=n.indexLabelTextBlock.y+k,b=0,b="right"===n.hemisphere?x.x+Math.sqrt(Math.pow(r,2)-Math.pow(d-x.y,2)):x.x-Math.sqrt(Math.pow(r,2)-Math.pow(d-x.y,2)),n.midAngle>Math.PI/2-t&&n.midAnglem.indexLabelTextBlock.x?b=m.indexLabelTextBlock.x-15:"right"===n.hemisphere&&("left"===a.hemisphere&&b3*Math.PI/2-t&&n.midAngle<3*Math.PI/2+t&&(m=(a-1+f.length)%f.length,m=f[m],a=f[(a+1+f.length)%f.length],"right"===n.hemisphere&&"left"===m.hemisphere&&ba.indexLabelTextBlock.x)&&(b=a.indexLabelTextBlock.x- +15)),n.indexLabelTextBlock.y=d,n.indexLabelTextBlock.x=b,n.indexLabelAngle=Math.atan2(n.indexLabelTextBlock.y-x.y,n.indexLabelTextBlock.x-x.x))}return k}function k(){var a=h.plotArea.ctx;a.fillStyle="grey";a.strokeStyle="grey";a.font="16px Arial";a.textBaseline="middle";for(var b=a=0,d=0,k=!0,b=0;10>b&&(1>b||0z){for(var E=u=0,H=0;Hu?l.indexLabelText="":l.indexLabelTextBlock.maxWidth=0.85*u,0.3*l.indexLabelTextBlock.maxWidthd&&(d=y)),y=y=0,0d&&(d=y)));var K=function(a, +b,c){for(var e=[],d=0;e.push(f[b]),b!==c;b=(b+1+s.length)%s.length);e.sort(function(a,b){return a.y-b.y});for(b=0;bz){n=t.indexLabelTextBlock.x;var h=t.indexLabelTextBlock.y-t.indexLabelTextBlock.height/ +2,w=t.indexLabelTextBlock.y+t.indexLabelTextBlock.height/2,p=m.indexLabelTextBlock.y-m.indexLabelTextBlock.height/2,u=m.indexLabelTextBlock.x+m.indexLabelTextBlock.width,r=m.indexLabelTextBlock.y+m.indexLabelTextBlock.height/2;n=t.indexLabelTextBlock.x+t.indexLabelTextBlock.widthu+q||h>r+q||wa&&(a=l),k!==a&&(b=k,d+=-z),0===l%Math.max(s.length/10,3)&&(g=!0)):g=!0;g&&(0=a.dataSeriesIndexes.length)){var m= +this.data[a.dataSeriesIndexes[0]],s=m.dataPoints,q=10,n=this.plotArea,f=m.dataPointEOs,p=2,r,v=1.3,t=20/180*Math.PI,C=6,x={x:(n.x2+n.x1)/2,y:(n.y2+n.y1)/2},z=0;a=!1;for(var y=0;ya&&(e=a,d=!0);var g=s[b].color?s[b].color:m._colorSet[b%m._colorSet.length];e>c&&ja(h.plotArea.ctx, +f[b].center,f[b].radius,g,m.type,c,e,m.fillOpacity,f[b].percentInnerRadius);if(d)break}l()},function(){h.disableToolTip=!1;h._animator.animate(0,h.animatedRender?500:0,function(a){b(a);l()})})}}};var ra=function(a,d,b,c){"undefined"===typeof b&&(b=1);0>=Math.round(d.y4-d.y1)||(a.save(),a.globalAlpha=b,a.beginPath(),a.moveTo(Math.round(d.x1),Math.round(d.y1)),a.lineTo(Math.round(d.x2),Math.round(d.y2)),a.lineTo(Math.round(d.x3),Math.round(d.y3)),a.lineTo(Math.round(d.x4),Math.round(d.y4)),"undefined"!== +d.x5&&(a.lineTo(Math.round(d.x5),Math.round(d.y5)),a.lineTo(Math.round(d.x6),Math.round(d.y6))),a.closePath(),a.fillStyle=c?c:d.color,a.fill(),a.globalAplha=1,a.restore())};p.prototype.renderFunnel=function(a){function d(){for(var a=0,b=[],c=0;ch?(h=c,m=(b+h)*(d-k)/2,a-=m,n=d-k,k+=d-k,n+=0==h?0:a/h,k+=a/h,m=!0):(n=(Math.abs(ba)*b-Math.sqrt(h))/2,h=b-2*n/Math.abs(ba),k+=n,k>d&&(k-=n, +h=c,m=(b+h)*(d-k)/2,a-=m,n=d-k,k+=d-k,n+=a/h,k+=a/h,m=!0),b=h)),e.push(n);return e}function c(){if(t&&C){for(var a,b,c,e,d,g,l,k,m,n,h,q,s,w,p=[],B=[],x={percent:null,total:null},v=null,y=0;yp[y]&&(p[y]=y!==fa?t.reversed?P[y].x3-P[y].x4:P[y].x2-P[y].x1:P[y].x2-P[y].x1,p[y]/=2));s=b.indexLabelMaxWidth?b.indexLabelMaxWidth:t.options.indexLabelMaxWidth?t.indexLabelMaxWidth:p[y];if(s>p[y]||0>s)s=p[y];B[y]="inside"===t.indexLabelPlacement?P[y].height:!1;x=z.getPercentAndTotal(t,b);if(t.indexLabelFormatter||b.indexLabelFormatter)v={chart:z.options,dataSeries:t,dataPoint:b,total:x.total,percent:x.percent};b=b.indexLabelFormatter?b.indexLabelFormatter(v):b.indexLabel? +z.replaceKeywordsWithValue(b.indexLabel,b,t,y):t.indexLabelFormatter?t.indexLabelFormatter(v):t.indexLabel?z.replaceKeywordsWithValue(t.indexLabel,b,t,y):b.label?b.label:"";0>=n&&(n=0);1E3>s&&1E3-sl?l:t.indexLabelMaxWidth:l,k=J.length-1;0<=k;k--){g=C[J[k].id];c=J[k];e=c.textBlock;b=(a=n(k)b.y&&(d=!0);c=g.indexLabelMaxWidth||l;if(c>l||0>c)c=l;f.push(c)}if(d)for(k=J.length-1;0<=k;k--)a=P[k],J[k].textBlock.maxWidth= +f[f.length-(k+1)],J[k].textBlock.measureText(),J[k].textBlock.x=L-l,c=J[k].textBlock.heightpa+D&&(J[k].textBlock.y=pa+D-J[k].height),J[k].textBlock.ywa+D&&(J[k].textBlock.y=wa+D-J[k].height))}function g(){var a,b,c,e;if("inside"!==t.indexLabelPlacement)for(var d=0;dDa?f(c).x2+1:(a.x2+a.x3)/2+1:(a.x2+a.x3)/2+1:"undefined"!==typeof a.x5?cpa+D&&(J[d].textBlock.y=pa+D-J[d].height),J[d].textBlock.ywa+D&&(J[d].textBlock.y=wa+D-J[d].height)));else for(d=0;d=c?(b=d!=fa?(a.x4+a.x3)/2-e/2:(a.x5+a.x4)/2-e/2,c=d!=fa?(a.y1+a.y3)/2-c/2:(a.y1+a.y4)/2-c/2,J[d].textBlock.x=b, +J[d].textBlock.y=c):J[d].isDirty=!0)}function k(){function a(b,c){var d;if(0>b||b>=J.length)return 0;var e,f=J[b].textBlock;if(0>c){c*=-1;e=q(b);d=l(e,b);if(d>=c)return f.y-=c,c;if(0==b)return 0=c)return f.y+=c,c;if(b==P.length-1)return 0e)&&(l=n(s),!(l>=J.length-1)&&J[s].textBlock.y+J[s].height+ga>J[l].textBlock.y&&(J[s].textBlock.y=J[s].textBlock.y+J[s].height-e>e-J[s].textBlock.y?e+1:e-J[s].height-1))}for(l=P.length-1;0e&&(e=0,J[e].isDirty))break;if(J[l].textBlock.y=f){f=0;k+=J[f].height;break}e=q(f); +if(0>e){f=0;k+=J[f].height;break}}if(f!=l){g=J[f].textBlock.y;a-=g;a=k-a;g=c(a,d,f);break}}}return g}function c(a,b,d){var e=[],f=0,g=0;for(a=Math.abs(a);d<=b;d++)e.push(P[d]);e.sort(function(a,b){return a.height-b.height});for(d=0;d+m.y.toFixed(6))&&(d=g.y+d+ga-m.y,e=a(w,-d),ea?t.reversed?wa-D:pa-D:J[a].textBlock.y+J[a].height+ga)}function h(a,b,c){var d,e,f,k=[],l=D,n=[];-1!==b&&(0<=W.indexOf(b)?(e=W.indexOf(b),W.splice(e,1)):(W.push(b),W=W.sort(function(a,b){return a-b})));if(0===W.length)k= +ia;else{e=D*(1!=W.length||0!=W[0]&&W[0]!=P.length-1?2:1)/m();for(var q=0;qn&&(n*=-1),c.y1+=b-n[d],c.y2+=b-n[d],c.y3+=b-n[d],c.y4+=b-n[d],c.y5&&(c.y5+=b-n[d],c.y6+=b-n[d]),n[d]=b}};a._animator.animate(0,c,function(c){var d=a.plotArea.ctx||a.ctx;ja=!0;d.clearRect(x.x1,x.y1,x.x2-x.x1,x.y2-x.y1);d.fillStyle=a.backgroundColor;d.fillRect(x.x1,x.y1,x.width,x.height);w.changeSection(c,b);var e={};e.dataSeries=t;e.dataPoint=t.reversed?t.dataPoints[C.length-1-b]:t.dataPoints[b];e.index=t.reversed?C.length-1-b:b;a.toolTip.highlightObjects([e]); +for(e=0;ea){b=P[c];break}return b?(a=b.y6?a>b.y6?b.x3+(b.x4-b.x3)/(b.y4-b.y3)*(a-b.y3):b.x2+(b.x3-b.x2)/(b.y3-b.y2)*(a-b.y2):b.x2+(b.x3-b.x2)/(b.y3-b.y2)*(a-b.y2), +{x1:a,x2:a}):-1}function p(a){for(var b=0;b=a.dataSeriesIndexes.length)){for(var t=this.data[a.dataSeriesIndexes[0]],C=t.dataPoints,x=this.plotArea,D=0.025*x.width,y=0.01*x.width,A=0,F=x.height-2*D,E=Math.min(x.width-2*y,2.8*x.height),H=!1,I=0;IF?N=F:0>=N&&(N=0),G>a?G=a-0.5:0>=G&&(G=0)):"pyramid"===t.type&&(G=N=0,t.reversed=t.reversed?!1:!0);var y=I+a/2,$=I,V=I+a,pa=t.reversed?Z:O,K=y-G/2,ea=y+G/2,Da=t.reversed?O+N:Z- +N,wa=t.reversed?O:Z;a=[];var y=[],P=[],E=[],X=O,fa,ba=(Da-pa)/(K-$),ha=-ba,I="area"===(t.valueRepresents?t.valueRepresents:"height")?b():d();if(-1!==I){if(t.reversed)for(E.push(X),G=I.length-1;0a&&(A=a));for(G=0;G\n');c.document.close();setTimeout(function(){c.focus();c.print();setTimeout(function(){b._canvasJSContainer.removeChild(d)},1E3)},500)};p.prototype.getPercentAndTotal=function(a,d){var b=null,c=null, +e=null;if(0<=a.type.indexOf("stacked"))c=0,b=d.x.getTime?d.x.getTime():d.x,b in a.plotUnit.yTotals&&(c=a.plotUnit.yTotals[b],e=isNaN(d.y)?0:100*(d.y/c));else if("pie"===a.type||"doughnut"===a.type||"funnel"===a.type||"pyramid"===a.type){for(b=c=0;b=k||"undefined"=== +typeof k||0>=v||"undefined"===typeof v)){if("horizontal"===this.orientation){n.textBlock=new ka(this.ctx,{x:0,y:0,maxWidth:v,maxHeight:this.itemWrap?k:this.lineHeight,angle:0,text:n.text,horizontalAlign:"left",fontSize:this.fontSize,fontFamily:this.fontFamily,fontWeight:this.fontWeight,fontColor:this.fontColor,fontStyle:this.fontStyle,textBaseline:"middle"});n.textBlock.measureText();null!==this.itemWidth&&(n.textBlock.width=this.itemWidth-(r+l+("line"===n.chartType||"spline"===n.chartType||"stepLine"=== +n.chartType?2*0.1*this.lineHeight:0)));if(!q||q.width+Math.round(n.textBlock.width+r+l+(0===q.width?0:this.horizontalSpacing)+("line"===n.chartType||"spline"===n.chartType||"stepLine"===n.chartType?2*0.1*this.lineHeight:0))>g)q={items:[],width:0},m.push(q),this.height+=f,f=0;f=Math.max(f,n.textBlock.height)}else n.textBlock=new ka(this.ctx,{x:0,y:0,maxWidth:x,maxHeight:!0===this.itemWrap?k:1.5*this.fontSize,angle:0,text:n.text,horizontalAlign:"left",fontSize:this.fontSize,fontFamily:this.fontFamily, +fontWeight:this.fontWeight,fontColor:this.fontColor,fontStyle:this.fontStyle,textBaseline:"middle"}),n.textBlock.measureText(),null!==this.itemWidth&&(n.textBlock.width=this.itemWidth-(r+l+("line"===n.chartType||"spline"===n.chartType||"stepLine"===n.chartType?2*0.1*this.lineHeight:0))),this.height>0,0),this.dataPoints.length):0):(s=this.dataPoints[this.dataPoints.length-1].x-this.dataPoints[0].x,s=0>0,0),this.dataPoints.length):0));for(;;){g=0a?c.x/a:a/c.x: +Math.abs(c.x-a);qs-e&&s+e>=this.dataPoints.length)break;-1===k?(e++,k=1):k=-1}return d||b.dataPoint.x!==a?d&&null!==b.dataPoint?b:null:b};F.prototype.getDataPointAtXY=function(a,d,b){if(!this.dataPoints||0===this.dataPoints.length||athis.chart.plotArea.x2||dthis.chart.plotArea.y2)return null;b=b||!1;var c=[],e=0,g=0,k=1,l=!1,h=Infinity, +m=0,s=0,q=0;if("none"!==this.chart.plotInfo.axisPlacement)if(q=(this.chart.axisX[0]?this.chart.axisX[0]:this.chart.axisX2[0]).getXValueAt({x:a,y:d}),this.axisX.logarithmic)var n=Math.log(this.dataPoints[this.dataPoints.length-1].x/this.dataPoints[0].x),q=1>0,0),this.dataPoints.length):0;else n=this.dataPoints[this.dataPoints.length-1].x-this.dataPoints[0].x,q=0> +0,0),this.dataPoints.length):0;for(;;){g=0=n.x1&&(a<=n.x2&&d>=n.y1&&d<=n.y2)&&(c.push({dataPoint:f,dataPointIndex:g,dataSeries:this,distance:Math.min(Math.abs(n.x1- +a),Math.abs(n.x2-a),Math.abs(n.y1-d),Math.abs(n.y2-d))}),l=!0);break;case "line":case "stepLine":case "spline":case "area":case "stepArea":case "stackedArea":case "stackedArea100":case "splineArea":case "scatter":var u=na("markerSize",f,this)||4,r=b?20:u,p=Math.sqrt(Math.pow(n.x1-a,2)+Math.pow(n.y1-d,2));p<=r&&c.push({dataPoint:f,dataPointIndex:g,dataSeries:this,distance:p});n=Math.abs(n.x1-a);n<=h?h=n:0r&&(p=Math.atan2(d-u.y,a-u.x),0>p&&(p+=2*Math.PI),p=Number(((180*(p/Math.PI)%360+360)%360).toFixed(12)),u=Number(((180*(n.startAngle/Math.PI)%360+360)%360).toFixed(12)),r=Number(((180*(n.endAngle/Math.PI)%360+360)%360).toFixed(12)),0===r&&1=r&&0!==f.y&&(r+=360,pu&&pp.y1&&dp.y6?(g=p.x6+(p.x5-p.x6)/(p.y5-p.y6)*(d-p.y6),p=p.x3+(p.x4-p.x3)/(p.y4-p.y3)*(d-p.y3)):(g=p.x1+(p.x6-p.x1)/(p.y6-p.y1)*(d-p.y1),p=p.x2+(p.x3-p.x2)/(p.y3-p.y2)*(d-p.y2)):(g=p.x1+(p.x4-p.x1)/(p.y4-p.y1)*(d-p.y1),p=p.x2+(p.x3-p.x2)/(p.y3-p.y2)*(d-p.y2)),a>g&&a=n.x1-n.borderThickness/2&&a<=n.x2+n.borderThickness/2&&d>=n.y4-n.borderThickness/2&&d<=n.y1+n.borderThickness/ +2||Math.abs(n.x2-a+n.x1-a)=n.y1&&d<=n.y4)c.push({dataPoint:f,dataPointIndex:g,dataSeries:this,distance:Math.min(Math.abs(n.x1-a),Math.abs(n.x2-a),Math.abs(n.y2-d),Math.abs(n.y3-d))}),l=!0;break;case "candlestick":if(a>=n.x1-n.borderThickness/2&&a<=n.x2+n.borderThickness/2&&d>=n.y2-n.borderThickness/2&&d<=n.y3+n.borderThickness/2||Math.abs(n.x2-a+n.x1-a)=n.y1&&d<=n.y4)c.push({dataPoint:f,dataPointIndex:g,dataSeries:this,distance:Math.min(Math.abs(n.x1-a), +Math.abs(n.x2-a),Math.abs(n.y2-d),Math.abs(n.y3-d))}),l=!0;break;case "ohlc":if(Math.abs(n.x2-a+n.x1-a)=n.y2&&d<=n.y3||a>=n.x1&&a<=(n.x2+n.x1)/2&&d>=n.y1-n.borderThickness/2&&d<=n.y1+n.borderThickness/2||a>=(n.x1+n.x2)/2&&a<=n.x2&&d>=n.y4-n.borderThickness/2&&d<=n.y4+n.borderThickness/2)c.push({dataPoint:f,dataPointIndex:g,dataSeries:this,distance:Math.min(Math.abs(n.x1-a),Math.abs(n.x2-a),Math.abs(n.y2-d),Math.abs(n.y3-d))}),l=!0}if(l||1E3q-e&&q+e>= +this.dataPoints.length)break;-1===k?(e++,k=1):k=-1}a=null;for(d=0;dq[f].endValue;f++);a=f=q[f].startValue&&b<=q[f].endValue;p=b;a||(a=this.labelFormatter?this.labelFormatter({chart:this.chart,axis:this.options,value:p,label:this.labels[p]?this.labels[p]:null}):"axisX"===this.type&&this.labels[p]?this.labels[p]:ba(p,this.valueFormatString,this.chart._cultureInfo),a=new ka(this.ctx,{x:0,y:0,maxWidth:g,maxHeight:k,angle:this.labelAngle,text:this.prefix+a+this.suffix,backgroundColor:this.labelBackgroundColor,borderColor:this.labelBorderColor,borderThickness:this.labelBorderThickness,cornerRadius:this.labelCornerRadius, +horizontalAlign:"left",fontSize:this.labelFontSize,fontFamily:this.labelFontFamily,fontWeight:this.labelFontWeight,fontColor:this.labelFontColor,fontStyle:this.labelFontStyle,textBaseline:"middle",borderThickness:0}),this._labels.push({position:p,textBlock:a,effectiveHeight:null}))}f=n;for(b=this.intervalStartPosition;b<=e;b=parseFloat(1E-12>this.interval?this.logarithmic&&this.equidistantInterval?b*Math.pow(this.logarithmBase,this.interval):b+this.interval:(this.logarithmic&&this.equidistantInterval? +b*Math.pow(this.logarithmBase,this.interval):b+this.interval).toFixed(12))){for(;fq[f].endValue;f++);a=f=q[f].startValue&&b<=q[f].endValue;p=b;a||(a=this.labelFormatter?this.labelFormatter({chart:this.chart,axis:this.options,value:p,label:this.labels[p]?this.labels[p]:null}):"axisX"===this.type&&this.labels[p]?this.labels[p]:ba(p,this.valueFormatString,this.chart._cultureInfo),a=new ka(this.ctx,{x:0,y:0,maxWidth:g,maxHeight:k,angle:this.labelAngle,text:this.prefix+a+this.suffix, +horizontalAlign:"left",backgroundColor:this.labelBackgroundColor,borderColor:this.labelBorderColor,borderThickness:this.labelBorderThickness,cornerRadius:this.labelCornerRadius,fontSize:this.labelFontSize,fontFamily:this.labelFontFamily,fontWeight:this.labelFontWeight,fontColor:this.labelFontColor,fontStyle:this.labelFontStyle,textBaseline:"middle"}),this._labels.push({position:p,textBlock:a,effectiveHeight:null}))}}else for(this.intervalStartPosition=this.getLabelStartPoint(new Date(this.viewportMinimum), +this.intervalType,this.interval),e=Ya(new Date(this.viewportMaximum),this.interval,this.intervalType),f=n,b=this.intervalStartPosition;bq[f].endValue;f++);p=a;a=f=q[f].startValue&&a<=q[f].endValue;a||(a=this.labelFormatter?this.labelFormatter({chart:this.chart,axis:this.options,value:new Date(p),label:this.labels[p]?this.labels[p]:null}):"axisX"===this.type&&this.labels[p]?this.labels[p]:Ca(p,this.valueFormatString,this.chart._cultureInfo), +a=new ka(this.ctx,{x:0,y:0,maxWidth:g,backgroundColor:this.labelBackgroundColor,borderColor:this.labelBorderColor,borderThickness:this.labelBorderThickness,cornerRadius:this.labelCornerRadius,maxHeight:k,angle:this.labelAngle,text:this.prefix+a+this.suffix,horizontalAlign:"left",fontSize:this.labelFontSize,fontFamily:this.labelFontFamily,fontWeight:this.labelFontWeight,fontColor:this.labelFontColor,fontStyle:this.labelFontStyle,textBaseline:"middle"}),this._labels.push({position:p,textBlock:a,effectiveHeight:null, +breaksLabelType:void 0}))}if("bottom"===this._position||"top"===this._position)l=this.logarithmic&&!this.equidistantInterval&&2<=this._labels.length?this.lineCoordinates.width*Math.log(Math.min(this._labels[this._labels.length-1].position/this._labels[this._labels.length-2].position,this._labels[1].position/this._labels[0].position))/Math.log(this.range):this.lineCoordinates.width/(this.logarithmic&&this.equidistantInterval?Math.log(this.range)/Math.log(this.logarithmBase):Math.abs(this.range))*S[this.intervalType+ +"Duration"]*this.interval,g="undefined"===typeof this.options.labelMaxWidth?0.5*this.chart.width>>0:this.options.labelMaxWidth,this.chart.panEnabled||(k="undefined"===typeof this.options.labelWrap||this.labelWrap?0.8*this.chart.height>>0:1.5*this.labelFontSize);else if("left"===this._position||"right"===this._position)l=this.logarithmic&&!this.equidistantInterval&&2<=this._labels.length?this.lineCoordinates.height*Math.log(Math.min(this._labels[this._labels.length-1].position/this._labels[this._labels.length- +2].position,this._labels[1].position/this._labels[0].position))/Math.log(this.range):this.lineCoordinates.height/(this.logarithmic&&this.equidistantInterval?Math.log(this.range)/Math.log(this.logarithmBase):Math.abs(this.range))*S[this.intervalType+"Duration"]*this.interval,this.chart.panEnabled||(g="undefined"===typeof this.options.labelMaxWidth?0.3*this.chart.width>>0:this.options.labelMaxWidth),k="undefined"===typeof this.options.labelWrap||this.labelWrap?0.3*this.chart.height>>0:1.5*this.labelFontSize; +for(c=0;cthis.labelAngle?this.labelAngle-=180:270<=this.labelAngle&&360>=this.labelAngle&&(this.labelAngle-=360)),"bottom"===this._position||"top"===this._position)if(g=0.9*l>>0,n=0,!this.chart.panEnabled&&1<=this._labels.length){this.sessionVariables.labelFontSize= +this.labelFontSize;this.sessionVariables.labelMaxWidth=g;this.sessionVariables.labelMaxHeight=k;this.sessionVariables.labelAngle=this.labelAngle;this.sessionVariables.labelWrap=this.labelWrap;for(b=0;bn&&(v=b,n=p.width)}b=0;for(b=this.intervalStartPosition< +this.viewportMinimum?1:0;b>0>2*g&&(this.sessionVariables.labelAngle=-25)):(this.sessionVariables.labelWrap=this.labelWrap,this.sessionVariables.labelMaxWidth=this.options.labelMaxWidth,this.sessionVariables.labelAngle=this.sessionVariables.labelMaxWidth>g?-25:this.sessionVariables.labelAngle):u(this.options.labelMaxWidth)?(this.sessionVariables.labelWrap=this.labelWrap,this.sessionVariables.labelMaxHeight=k,this.sessionVariables.labelMaxWidth= +g,B.width+d.width>>0>2*g&&(this.sessionVariables.labelAngle=-25,this.sessionVariables.labelMaxWidth=p)):(this.sessionVariables.labelAngle=this.sessionVariables.labelMaxWidth>g?-25:this.sessionVariables.labelAngle,this.sessionVariables.labelMaxWidth=this.options.labelMaxWidth,this.sessionVariables.labelMaxHeight=k,this.sessionVariables.labelWrap=this.labelWrap);else{if(u(this.options.labelWrap))if(!u(this.options.labelMaxWidth))this.options.labelMaxWidth>0,f=this.labelFontSize,nq&&(q=c-2*g,c>=2*g&&c<2.2*g?(this.sessionVariables.labelMaxWidth=g,u(this.options.labelFontSize)&&12=2.2*g&&c<2.8*g?(this.sessionVariables.labelAngle=-25,this.sessionVariables.labelMaxWidth=p,this.sessionVariables.labelFontSize=f):c>=2.8*g&&c<3.2*g?(this.sessionVariables.labelMaxWidth=Math.max(g,n),this.sessionVariables.labelWrap=!0,u(this.options.labelFontSize)&&12=3.2*g&&c<3.6*g?(this.sessionVariables.labelAngle=-25,this.sessionVariables.labelWrap=!0,this.sessionVariables.labelMaxWidth=p,this.sessionVariables.labelFontSize=this.labelFontSize):c>3.6*g&&c<5*g?(u(this.options.labelFontSize)&&125*g&&(this.sessionVariables.labelWrap=!0,this.sessionVariables.labelMaxWidth=g,this.sessionVariables.labelFontSize=f,this.sessionVariables.labelMaxHeight=k,this.sessionVariables.labelAngle=this.labelAngle));else if(v===b&&(0===v&&n+this._labels[v+1].textBlock.measureText().width-2*g>q||v===this._labels.length-1&&n+this._labels[v-1].textBlock.measureText().width-2*g>q||0q&&n+this._labels[v-1].textBlock.measureText().width- +2*g>q))q=0===v?n+this._labels[v+1].textBlock.measureText().width-2*g:n+this._labels[v-1].textBlock.measureText().width-2*g,this.sessionVariables.labelFontSize=u(this.options.labelFontSize)?f:this.options.labelFontSize,this.sessionVariables.labelWrap=!0,this.sessionVariables.labelAngle=-25,this.sessionVariables.labelMaxWidth=p;else if(0===q)for(this.sessionVariables.labelFontSize=u(this.options.labelFontSize)?f:this.options.labelFontSize,this.sessionVariables.labelWrap=!0,c=0;c>0>2*g&&(this.sessionVariables.labelAngle=-25))}else(this.sessionVariables.labelAngle=this.labelAngle,this.sessionVariables.labelMaxHeight=0===this.labelAngle?k:Math.min((c-g*Math.cos(Math.PI/180*Math.abs(this.labelAngle)))/ +Math.sin(Math.PI/180*Math.abs(this.labelAngle)),c),p=0!=this.labelAngle?(m-(h+a.fontSize/2)*Math.cos(Math.PI/180*Math.abs(this.labelAngle)))/Math.sin(Math.PI/180*Math.abs(this.labelAngle)):g,this.sessionVariables.labelMaxHeight=k=this.labelWrap?(m-p*Math.sin(Math.PI/180*Math.abs(this.labelAngle)))/Math.cos(Math.PI/180*Math.abs(this.labelAngle)):1.5*this.labelFontSize,u(this.options.labelWrap))?u(this.options.labelWrap)&&(this.labelWrap&&!u(this.options.labelMaxWidth)?(this.sessionVariables.labelWrap= +this.labelWrap,this.sessionVariables.labelMaxWidth=this.options.labelMaxWidth?this.options.labelMaxWidth:p,this.sessionVariables.labelMaxHeight=k):(this.sessionVariables.labelAngle=this.labelAngle,this.sessionVariables.labelMaxWidth=p,this.sessionVariables.labelMaxHeight=c<0.9*l?0.9*l:c,this.sessionVariables.labelWrap=this.labelWrap)):(this.options.labelWrap?(this.sessionVariables.labelWrap=this.labelWrap,this.sessionVariables.labelMaxWidth=this.options.labelMaxWidth?this.options.labelMaxWidth:p): +(u(this.options.labelMaxWidth),this.sessionVariables.labelMaxWidth=this.options.labelMaxWidth?this.options.labelMaxWidth:p,this.sessionVariables.labelWrap=this.labelWrap),this.sessionVariables.labelMaxHeight=k)}for(c=0;c>0:this.options.labelMaxWidth,k="undefined"===typeof this.options.labelWrap||this.labelWrap?0.3*this.chart.height>>0:1.5*this.labelFontSize,!this.chart.panEnabled&&1<=this._labels.length){this.sessionVariables.labelFontSize=this.labelFontSize;this.sessionVariables.labelMaxWidth= +g;this.sessionVariables.labelMaxHeight=k;this.sessionVariables.labelAngle=u(this.sessionVariables.labelAngle)?0:this.sessionVariables.labelAngle;this.sessionVariables.labelWrap=this.labelWrap;for(b=0;b>0,l-2*k>n&&(n=l-2*k,l>=2*k&&l<2.4*k?(u(this.options.labelFontSize)&&12=2.4*k&&l<2.8*k?(this.sessionVariables.labelMaxHeight=c,this.sessionVariables.labelFontSize=this.labelFontSize,this.sessionVariables.labelWrap=!0):l>=2.8*k&&l<3.2*k?(this.sessionVariables.labelMaxHeight=k,this.sessionVariables.labelWrap=!0,u(this.options.labelFontSize)&&12< +this.labelFontSize&&(this.labelFontSize=Math.floor(12/13*this.labelFontSize),a.measureText()),this.sessionVariables.labelFontSize=u(this.options.labelFontSize)?this.labelFontSize:this.options.labelFontSize,this.sessionVariables.labelAngle=u(this.sessionVariables.labelAngle)?0:this.sessionVariables.labelAngle):l>=3.2*k&&l<3.6*k?(this.sessionVariables.labelMaxHeight=c,this.sessionVariables.labelWrap=!0,this.sessionVariables.labelFontSize=this.labelFontSize):l>3.6*k&&l<10*k?(u(this.options.labelFontSize)&& +1210*k&&l<50*k&&(u(this.options.labelFontSize)&&12this.options.labelMaxWidth:this.sessionVariables.labelMaxWidth,this.sessionVariables.labelWrap=this.labelWrap,this.sessionVariables.labelMaxHeight=c):(this.sessionVariables.labelMaxWidth=this.options.labelMaxWidth?this.options.labelMaxWidth:g,this.sessionVariables.labelMaxHeight=0===this.labelAngle?k:c,u(this.options.labelMaxWidth)&& +(this.sessionVariables.labelAngle=this.labelAngle))):this.options.labelWrap?(this.sessionVariables.labelMaxHeight=0===this.labelAngle?k:c,this.sessionVariables.labelWrap=this.labelWrap,this.sessionVariables.labelMaxWidth=g):(this.sessionVariables.labelMaxHeight=k,u(this.options.labelMaxWidth),this.sessionVariables.labelMaxWidth=this.options.labelMaxWidth?this.options.labelMaxWidth:this.sessionVariables.labelMaxWidth,this.sessionVariables.labelWrap=this.labelWrap)}for(c=0;c>0:1.5*this.labelFontSize;if("left"===this._position||"right"===this._position)z=u(g.options.labelWrap)?this.sessionVariables.labelMaxHeight:g.labelWrap?0.8*this.chart.width>>0:1.5*this.labelFontSize;u(g.labelBackgroundColor)&&(g.labelBackgroundColor="#EEEEEE")}else k="bottom"===this._position||"top"===this._position?0.9*this.chart.width>>0:0.9*this.chart.height>> +0,z=u(g.options.labelWrap)||g.labelWrap?"bottom"===this._position||"top"===this._position?0.8*this.chart.width>>0:0.8*this.chart.height>>0:1.5*this.labelFontSize,u(g.labelBackgroundColor)&&(u(g.startValue)&&0!==g.startValue?g.labelBackgroundColor=r?"transparent":null:g.labelBackgroundColor="#EEEEEE");a=new ka(this.ctx,{x:0,y:0,backgroundColor:g.labelBackgroundColor,borderColor:g.labelBorderColor,borderThickness:g.labelBorderThickness,cornerRadius:g.labelCornerRadius,maxWidth:g.options.labelMaxWidth? +g.options.labelMaxWidth:k,maxHeight:z,angle:this.labelAngle,text:g.labelFormatter?g.labelFormatter({chart:this.chart,axis:this,stripLine:g}):g.label,horizontalAlign:"left",fontSize:"outside"===g.labelPlacement?g.options.labelFontSize?g.labelFontSize:this.labelFontSize:g.labelFontSize,fontFamily:"outside"===g.labelPlacement?g.options.labelFontFamily?g.labelFontFamily:this.labelFontFamily:g.labelFontFamily,fontWeight:"outside"===g.labelPlacement?g.options.labelFontWeight?g.labelFontWeight:this.labelFontWeight: +g.labelFontWeight,fontColor:g.labelFontColor||g.color,fontStyle:"outside"===g.labelPlacement?g.options.labelFontStyle?g.labelFontStyle:this.fontWeight:g.labelFontStyle,textBaseline:"middle"});this._stripLineLabels.push({position:g.value,textBlock:a,effectiveHeight:null,stripLine:g})}};z.prototype.createLabelsAndCalculateWidth=function(){var a=0,d=0;this._labels=[];this._stripLineLabels=[];var b=this.chart.isNavigator?0:5;if("left"===this._position||"right"===this._position){this.createLabels();for(d= +0;d=this.viewportMinimum&&this._stripLineLabels[d].stripLine.value<=this.viewportMaximum)&& +(c=this._stripLineLabels[d].textBlock,e=c.measureText(),g=0===this.labelAngle?e.width:e.width*Math.cos(Math.PI/180*Math.abs(this.labelAngle))+(e.height-c.fontSize/2)*Math.sin(Math.PI/180*Math.abs(this.labelAngle)),a=this.viewportMinimum&&this._stripLineLabels[b].stripLine.value<=this.viewportMaximum)&&(d=this._stripLineLabels[b].textBlock,e=d.measureText(),g=0===this.labelAngle?e.height:e.width*Math.sin(Math.PI/180*Math.abs(this.labelAngle))+(e.height-d.fontSize/2)*Math.cos(Math.PI/180*Math.abs(this.labelAngle)),an[f].viewportMaximum);v++)r[v].endValue=n[f].viewPortMinimum&&(n[f].scaleBreaks.lastBreakIndex=v));for(var z=v=0,t=0,C=0,x=0,D=0,y=0,A,E,F=l=0,H,I,L,r=H=I=L=!1,f=0;fv;){var G=0,R=0,S=0,U=0,W=e=0,K=0,$=0,V=0,X=0,P=0,ba=0;if(b&& +0p.width- +q?p.width-q:g.x2-ba-$);if(a&&0p.width-q?p.width-q:g.x2-ba-$),a[f]._labels&&1h&&(l+=0a[f].labelAngle?A-zh&&(l=E+t/2-h-ba),A-za[f].labelAngle&&0p.width-q?p.width-q:g.x2-ba-$),d[f].lineCoordinates.width=Math.abs(h-k),d[f]._labels&&1v;){V=U=R=S=$=K=W=e=Q=O=G=X=0;if(a&&0p.width-10?p.width-10:g.x2-V-W),b[f].labelAutoFit&&!u(C)&&(0b[f].labelAngle?Math.max(k,C):0===b[f].labelAngle? +Math.max(k,C/2):k),0c[f].chart.width-10?c[f].chart.width-10:g.x2-V-W),c[f]&& +c[f].labelAutoFit&&!u(D)&&(0b[f].chart.height-10?b[f].chart.height-10:g.y2),b[f].lineCoordinates.y1=l-(q[f]+b[f].margin+ +X),b[f].lineCoordinates.y2=l-(q[f]+b[f].margin+X),b[f].bounds={x1:k,y1:l-(q[f]+X+b[f].margin),x2:h,y2:m-(X+b[f].margin),width:h-k,height:q[f]},b[f].title&&(b[f]._titleTextBlock.maxWidth=0p.height-Math.max(K,10)?p.height-Math.max(K,10):g.y2-S):g.y2>p.height-Math.max(K,10)?p.height-Math.max(K,10):g.y2;if(b&&0b[K].labelAngle?Math.max(h,C):0===b[K].labelAngle?Math.max(h,C/2):h,k=0>b[K].labelAngle||0===b[K].labelAngle?h-U:k);if(c&&0p.height-Math.max(K,10)?p.height-Math.max(K,10):g.y2-S):g.y2>p.height-Math.max(K,10)?p.height-Math.max(K,10):g.y2;if(b&&0b[K].labelAngle?Math.max(h,C):0===b[K].labelAngle?Math.max(h,C/2):h,k=0>b[K].labelAngle||0=== +b[K].labelAngle?h-V:k);if(c&&0d[g].spacing?0:Math.abs(d[g].spacing/b),this.logarithmic&&(d[g].size=Math.pow(this.logarithmBase,d[g].size))};z.prototype.calculateBreaksInPixels=function(){if(!(this.scaleBreaks&&0>=this.scaleBreaks._appliedBreaks.length)){var a=this.scaleBreaks?this.scaleBreaks._appliedBreaks:[];a.length&&(this.scaleBreaks.firstBreakIndex=this.scaleBreaks.lastBreakIndex=null);for(var d=0;dthis.conversionParameters.maximum);d++)a[d].endValue< +this.conversionParameters.minimum||(u(this.scaleBreaks.firstBreakIndex)&&(this.scaleBreaks.firstBreakIndex=d),a[d].startValue>=this.conversionParameters.minimum&&(a[d].startPixel=this.convertValueToPixel(a[d].startValue),this.scaleBreaks.lastBreakIndex=d),a[d].endValue<=this.conversionParameters.maximum&&(a[d].endPixel=this.convertValueToPixel(a[d].endValue)))}};z.prototype.renderLabelsTicksAndTitle=function(){var a=this,d=!1,b=0,c=0,e=1,g=0;0!==this.labelAngle&&360!==this.labelAngle&&(e=1.2);if("undefined"=== +typeof this.options.interval){if("bottom"===this._position||"top"===this._position)if(this.logarithmic&&!this.equidistantInterval&&this.labelAutoFit){for(var b=[],e=0!==this.labelAngle&&360!==this.labelAngle?1:1.2,k,l=this.viewportMaximum,h=this.lineCoordinates.width/Math.log(this.range),m=this._labels.length-1;0<=m;m--){q=this._labels[m];if(q.positionthis.viewportMaximum||!(m===this._labels.length-1||kthis.lineCoordinates.width*e&&this.labelAutoFit&&(d=!0)}if("left"===this._position||"right"===this._position)if(this.logarithmic&& +!this.equidistantInterval&&this.labelAutoFit){for(var b=[],p,l=this.viewportMaximum,h=this.lineCoordinates.height/Math.log(this.range),m=this._labels.length-1;0<=m;m--){q=this._labels[m];if(q.positionthis.viewportMaximum||!(m===this._labels.length-1||pthis.lineCoordinates.height*e&&this.labelAutoFit&&(d=!0)}}this.logarithmic&&(!this.equidistantInterval&&this.labelAutoFit)&&this._labels.sort(function(a,b){return a.position-b.position});var m=0,q,n;if("bottom"===this._position){for(m=0;mthis.viewportMaximum||d&&0!==g++%2&&this.labelAutoFit)||(n=this.getPixelCoordinatesOnAxis(q.position),this.tickThickness&&"inside"!=this.labelPlacement&&(this.ctx.lineWidth=this.tickThickness,this.ctx.strokeStyle=this.tickColor,c=1===this.ctx.lineWidth%2?(n.x<<0)+0.5:n.x<<0,this.ctx.beginPath(),this.ctx.moveTo(c,n.y<<0),this.ctx.lineTo(c,n.y+this.tickLength<<0),this.ctx.stroke()),0===q.textBlock.angle?(n.x-=q.textBlock.width/2,n.y="inside"===this.labelPlacement? +n.y-(this.tickLength+q.textBlock.fontSize/2):n.y+this.tickLength+q.textBlock.fontSize/2):(n.x="inside"===this.labelPlacement?0>this.labelAngle?n.x:n.x-q.textBlock.width*Math.cos(Math.PI/180*this.labelAngle):n.x-(0>this.labelAngle?q.textBlock.width*Math.cos(Math.PI/180*this.labelAngle):0),n.y="inside"===this.labelPlacement?0>this.labelAngle?n.y-this.tickLength-5:n.y-this.tickLength-Math.abs(q.textBlock.width*Math.sin(Math.PI/180*this.labelAngle)+5):n.y+this.tickLength+Math.abs(0>this.labelAngle?q.textBlock.width* +Math.sin(Math.PI/180*this.labelAngle)-5:5)),q.textBlock.x=n.x,q.textBlock.y=n.y);"inside"===this.labelPlacement&&this.chart.addEventListener("dataAnimationIterationEnd",function(){for(m=0;ma.viewportMaximum||d&&0!==g++%2&&a.labelAutoFit)&&(n=a.getPixelCoordinatesOnAxis(q.position),a.tickThickness)){a.ctx.lineWidth=a.tickThickness;a.ctx.strokeStyle=a.tickColor;var b=1===a.ctx.lineWidth%2?(n.x<<0)+0.5:n.x<<0;a.ctx.save(); +a.ctx.beginPath();a.ctx.moveTo(b,n.y<<0);a.ctx.lineTo(b,n.y-a.tickLength<<0);a.ctx.stroke();a.ctx.restore()}},this);this.title&&(this._titleTextBlock.measureText(),this._titleTextBlock.x=this.lineCoordinates.x1+this.lineCoordinates.width/2-this._titleTextBlock.width/2,this._titleTextBlock.y=this.bounds.y2-this._titleTextBlock.height-3,this.titleMaxWidth=this._titleTextBlock.maxWidth,this._titleTextBlock.render(!0))}else if("top"===this._position){for(m=0;mthis.viewportMaximum||d&&0!==g++%2&&this.labelAutoFit)||(n=this.getPixelCoordinatesOnAxis(q.position),this.tickThickness&&"inside"!=this.labelPlacement&&(this.ctx.lineWidth=this.tickThickness,this.ctx.strokeStyle=this.tickColor,c=1===this.ctx.lineWidth%2?(n.x<<0)+0.5:n.x<<0,this.ctx.beginPath(),this.ctx.moveTo(c,n.y<<0),this.ctx.lineTo(c,n.y-this.tickLength<<0),this.ctx.stroke()),0===q.textBlock.angle?(n.x-=q.textBlock.width/2,n.y="inside"===this.labelPlacement? +n.y+this.labelFontSize/2+this.tickLength+5:n.y-(this.tickLength+q.textBlock.height-q.textBlock.fontSize/2)):(n.x="inside"===this.labelPlacement?0a.viewportMaximum||d&&0!==g++%2&&a.labelAutoFit)&& +(n=a.getPixelCoordinatesOnAxis(q.position),a.tickThickness)){a.ctx.lineWidth=a.tickThickness;a.ctx.strokeStyle=a.tickColor;var b=1===this.ctx.lineWidth%2?(n.x<<0)+0.5:n.x<<0;a.ctx.save();a.ctx.beginPath();a.ctx.moveTo(b,n.y<<0);a.ctx.lineTo(b,n.y+a.tickLength<<0);a.ctx.stroke();a.ctx.restore()}},this);this.title&&(this._titleTextBlock.measureText(),this._titleTextBlock.x=this.lineCoordinates.x1+this.lineCoordinates.width/2-this._titleTextBlock.width/2,this._titleTextBlock.y=this.bounds.y1+1,this.titleMaxWidth= +this._titleTextBlock.maxWidth,this._titleTextBlock.render(!0))}else if("left"===this._position){for(m=0;mthis.viewportMaximum||d&&0!==g++%2&&this.labelAutoFit)||(n=this.getPixelCoordinatesOnAxis(q.position),this.tickThickness&&"inside"!=this.labelPlacement&&(this.ctx.lineWidth=this.tickThickness,this.ctx.strokeStyle=this.tickColor,c=1===this.ctx.lineWidth%2?(n.y<<0)+0.5:n.y<<0,this.ctx.beginPath(),this.ctx.moveTo(n.x<< +0,c),this.ctx.lineTo(n.x-this.tickLength<<0,c),this.ctx.stroke()),0===this.labelAngle?(q.textBlock.y=n.y,q.textBlock.x="inside"===this.labelPlacement?n.x+this.tickLength+5:n.x-q.textBlock.width*Math.cos(Math.PI/180*this.labelAngle)-this.tickLength-5):(q.textBlock.y="inside"===this.labelPlacement?n.y:n.y-q.textBlock.width*Math.sin(Math.PI/180*this.labelAngle),q.textBlock.x="inside"===this.labelPlacement?n.x+this.tickLength+5:0a.viewportMaximum||d&&0!==g++%2&&a.labelAutoFit)&&(n=a.getPixelCoordinatesOnAxis(q.position),a.tickThickness)){a.ctx.lineWidth=a.tickThickness; +a.ctx.strokeStyle=a.tickColor;var b=1===a.ctx.lineWidth%2?(n.y<<0)+0.5:n.y<<0;a.ctx.save();a.ctx.beginPath();a.ctx.moveTo(n.x<<0,b);a.ctx.lineTo(n.x+a.tickLength<<0,b);a.ctx.stroke();a.ctx.restore()}},this);this.title&&(this._titleTextBlock.measureText(),this._titleTextBlock.x=this.bounds.x1+1,this._titleTextBlock.y=this.lineCoordinates.height/2+this._titleTextBlock.width/2+this.lineCoordinates.y1,this.titleMaxWidth=this._titleTextBlock.maxWidth,this._titleTextBlock.render(!0))}else if("right"=== +this._position){for(m=0;mthis.viewportMaximum||d&&0!==g++%2&&this.labelAutoFit)||(n=this.getPixelCoordinatesOnAxis(q.position),this.tickThickness&&"inside"!=this.labelPlacement&&(this.ctx.lineWidth=this.tickThickness,this.ctx.strokeStyle=this.tickColor,c=1===this.ctx.lineWidth%2?(n.y<<0)+0.5:n.y<<0,this.ctx.beginPath(),this.ctx.moveTo(n.x<<0,c),this.ctx.lineTo(n.x+this.tickLength<<0,c),this.ctx.stroke()),0===this.labelAngle? +(q.textBlock.y=n.y,q.textBlock.x="inside"===this.labelPlacement?n.x-q.textBlock.width-this.tickLength-5:n.x+this.tickLength+5):(q.textBlock.y="inside"===this.labelPlacement?n.y-q.textBlock.width*Math.sin(Math.PI/180*this.labelAngle):0>this.labelAngle?n.y:n.y-(q.textBlock.height-q.textBlock.fontSize/2-5)*Math.cos(Math.PI/180*this.labelAngle),q.textBlock.x="inside"===this.labelPlacement?n.x-q.textBlock.width*Math.cos(Math.PI/180*this.labelAngle)-this.tickLength-5:0a.viewportMaximum||d&&0!==g++%2&&a.labelAutoFit)&&(n=a.getPixelCoordinatesOnAxis(q.position),a.tickThickness)){a.ctx.lineWidth=a.tickThickness;a.ctx.strokeStyle=a.tickColor;var b=1===a.ctx.lineWidth%2?(n.y<< +0)+0.5:n.y<<0;a.ctx.save();a.ctx.beginPath();a.ctx.moveTo(n.x<<0,b);a.ctx.lineTo(n.x-a.tickLength<<0,b);a.ctx.stroke();a.ctx.restore()}},this);this.title&&(this._titleTextBlock.measureText(),this._titleTextBlock.x=this.bounds.x2-1,this._titleTextBlock.y=this.lineCoordinates.height/2-this._titleTextBlock.width/2+this.lineCoordinates.y1,this.titleMaxWidth=this._titleTextBlock.maxWidth,this._titleTextBlock.render(!0))}g=0;if("inside"===this.labelPlacement)this.chart.addEventListener("dataAnimationIterationEnd", +function(){for(m=0;ma.viewportMaximum||d&&0!==g++%2&&a.labelAutoFit)||(a.ctx.save(),a.ctx.beginPath(),q.textBlock.render(!0),a.ctx.restore())},this);else for(m=0;mthis.viewportMaximum||d&&0!==g++%2&&this.labelAutoFit)||q.textBlock.render(!0)};z.prototype.renderInterlacedColors=function(){var a=this.chart.plotArea.ctx,d,b,c=this.chart.plotArea, +e=0;d=!0;if(("bottom"===this._position||"top"===this._position)&&this.interlacedColor)for(a.fillStyle=this.interlacedColor,e=0;ethis._labels.length-1?this.getPixelCoordinatesOnAxis(this.viewportMaximum):this.getPixelCoordinatesOnAxis(this._labels[e+1].position),a.fillRect(Math.min(b.x,d.x),c.y1,Math.abs(b.x-d.x),Math.abs(c.y1-c.y2)),d=!1):d=!0;else if(("left"===this._position||"right"===this._position)&&this.interlacedColor)for(a.fillStyle= +this.interlacedColor,e=0;ethis._labels.length-1?this.getPixelCoordinatesOnAxis(this.viewportMaximum):this.getPixelCoordinatesOnAxis(this._labels[e+1].position),a.fillRect(c.x1,Math.min(b.y,d.y),Math.abs(c.x1-c.x2),Math.abs(d.y-b.y)),d=!1):d=!0;a.beginPath()};z.prototype.renderStripLinesOfThicknessType=function(a){if(this.stripLines&&0this.viewportMaximum||u(m.value)||isNaN(this.range))||l.push(m))}for(c=0;cthis.viewportMaximum||isNaN(this.range))){a=this.getPixelCoordinatesOnAxis(b.position);if("outside"===b.stripLine.labelPlacement)if(m&&(this.ctx.strokeStyle= +m.color,"pixel"===m._thicknessType&&(this.ctx.lineWidth=m.thickness)),"bottom"===this._position){var p=1===this.ctx.lineWidth%2?(a.x<<0)+0.5:a.x<<0;this.ctx.beginPath();this.ctx.moveTo(p,a.y<<0);this.ctx.lineTo(p,a.y+this.tickLength<<0);this.ctx.stroke();0===this.labelAngle?(a.x-=b.textBlock.width/2,a.y+=this.tickLength+b.textBlock.fontSize/2):(a.x-=0>this.labelAngle?b.textBlock.width*Math.cos(Math.PI/180*this.labelAngle):0,a.y+=this.tickLength+Math.abs(0>this.labelAngle?b.textBlock.width*Math.sin(Math.PI/ +180*this.labelAngle)-5:5))}else"top"===this._position?(p=1===this.ctx.lineWidth%2?(a.x<<0)+0.5:a.x<<0,this.ctx.beginPath(),this.ctx.moveTo(p,a.y<<0),this.ctx.lineTo(p,a.y-this.tickLength<<0),this.ctx.stroke(),0===this.labelAngle?(a.x-=b.textBlock.width/2,a.y-=this.tickLength+b.textBlock.height):(a.x+=(b.textBlock.height-this.tickLength-this.labelFontSize/2)*Math.sin(Math.PI/180*this.labelAngle)-(0this.labelAngle?a.y:a.y-(b.textBlock.height-b.textBlock.fontSize/ +2-5)*Math.cos(Math.PI/180*this.labelAngle),a.x=0this.chart.plotArea.x1?u(m.startValue)?a.x-=b.textBlock.height-b.textBlock.fontSize/ +2:a.x-=b.textBlock.height/2-b.textBlock.fontSize/2+3:(b.textBlock.angle=90,u(m.startValue)?a.x+=b.textBlock.height-b.textBlock.fontSize/2:a.x+=b.textBlock.height/2-b.textBlock.fontSize/2+3),a.y=-90===b.textBlock.angle?"near"===b.stripLine.labelAlign?this.chart.plotArea.y2-3:"center"===b.stripLine.labelAlign?(this.chart.plotArea.y2+this.chart.plotArea.y1+b.textBlock.width)/2:this.chart.plotArea.y1+b.textBlock.width+3:"near"===b.stripLine.labelAlign?this.chart.plotArea.y2-b.textBlock.width-3:"center"=== +b.stripLine.labelAlign?(this.chart.plotArea.y2+this.chart.plotArea.y1-b.textBlock.width)/2:this.chart.plotArea.y1+3):"top"===this._position?(b.textBlock.maxWidth=this.options.stripLines[c].labelMaxWidth?this.options.stripLines[c].labelMaxWidth:this.chart.plotArea.height-3,b.textBlock.measureText(),a.x-b.textBlock.height>this.chart.plotArea.x1?u(m.startValue)?a.x-=b.textBlock.height-b.textBlock.fontSize/2:a.x-=b.textBlock.height/2-b.textBlock.fontSize/2+3:(b.textBlock.angle=90,u(m.startValue)?a.x+= +b.textBlock.height-b.textBlock.fontSize/2:a.x+=b.textBlock.height/2-b.textBlock.fontSize/2+3),a.y=-90===b.textBlock.angle?"near"===b.stripLine.labelAlign?this.chart.plotArea.y1+b.textBlock.width+3:"center"===b.stripLine.labelAlign?(this.chart.plotArea.y2+this.chart.plotArea.y1+b.textBlock.width)/2:this.chart.plotArea.y2-3:"near"===b.stripLine.labelAlign?this.chart.plotArea.y1+3:"center"===b.stripLine.labelAlign?(this.chart.plotArea.y2+this.chart.plotArea.y1-b.textBlock.width)/2:this.chart.plotArea.y2- +b.textBlock.width-3):"left"===this._position?(b.textBlock.maxWidth=this.options.stripLines[c].labelMaxWidth?this.options.stripLines[c].labelMaxWidth:this.chart.plotArea.width-3,b.textBlock.angle=0,b.textBlock.measureText(),a.y-b.textBlock.height>this.chart.plotArea.y1?u(m.startValue)?a.y-=b.textBlock.height-b.textBlock.fontSize/2:a.y-=b.textBlock.height/2-b.textBlock.fontSize+3:a.y-b.textBlock.heightthis.chart.plotArea.y1? +u(m.startValue)?a.y-=b.textBlock.height-b.textBlock.fontSize/2:a.y-=b.textBlock.height/2-b.textBlock.fontSize/2-3:a.y-b.textBlock.heightthis.viewportMaximum|| +isNaN(this.range))||a[d].render(this.maskCtx);this.maskCtx.restore()}};z.prototype.renderCrosshair=function(a,d){this.crosshair.render(a,d)};z.prototype.renderGrid=function(){if(this.gridThickness&&0this.viewportMaximum||this._labels[c].breaksLabelType)||(a.beginPath(),d=this.getPixelCoordinatesOnAxis(this._labels[c].position),d=1===a.lineWidth%2?(d.x<<0)+0.5:d.x<<0,a.moveTo(d,b.y1<<0),a.lineTo(d,b.y2<<0),a.stroke());else if("left"===this._position||"right"===this._position)for(var c=0;cthis.viewportMaximum||this._labels[c].breaksLabelType)||(a.beginPath(), +d=this.getPixelCoordinatesOnAxis(this._labels[c].position),d=1===a.lineWidth%2?(d.y<<0)+0.5:d.y<<0,a.moveTo(b.x1<<0,d),a.lineTo(b.x2<<0,d),a.stroke());a.restore()}};z.prototype.renderAxisLine=function(){var a=this.chart.ctx,d=r?this.chart._preRenderCtx:a,b=Math.ceil(this.tickThickness/(this.reversed?-2:2)),c=Math.ceil(this.tickThickness/(this.reversed?2:-2)),e,g;d.save();if("bottom"===this._position||"top"===this._position){if(this.lineThickness){this.reversed?(e=this.lineCoordinates.x2,g=this.lineCoordinates.x1): +(e=this.lineCoordinates.x1,g=this.lineCoordinates.x2);d.lineWidth=this.lineThickness;d.strokeStyle=this.lineColor?this.lineColor:"black";d.setLineDash&&d.setLineDash(R(this.lineDashType,this.lineThickness));var k=1===this.lineThickness%2?(this.lineCoordinates.y1<<0)+0.5:this.lineCoordinates.y1<<0;d.beginPath();if(this.scaleBreaks&&!u(this.scaleBreaks.firstBreakIndex))if(u(this.scaleBreaks.lastBreakIndex))e=this.scaleBreaks._appliedBreaks[this.scaleBreaks.firstBreakIndex].endPixel+c;else for(var l= +this.scaleBreaks.firstBreakIndex;l<=this.scaleBreaks.lastBreakIndex;l++)d.moveTo(e,k),d.lineTo(this.scaleBreaks._appliedBreaks[l].startPixel+b,k),e=this.scaleBreaks._appliedBreaks[l].endPixel+c;e&&(d.moveTo(e,k),d.lineTo(g,k));d.stroke()}}else if(("left"===this._position||"right"===this._position)&&this.lineThickness){this.reversed?(e=this.lineCoordinates.y1,g=this.lineCoordinates.y2):(e=this.lineCoordinates.y2,g=this.lineCoordinates.y1);d.lineWidth=this.lineThickness;d.strokeStyle=this.lineColor; +d.setLineDash&&d.setLineDash(R(this.lineDashType,this.lineThickness));k=1===this.lineThickness%2?(this.lineCoordinates.x1<<0)+0.5:this.lineCoordinates.x1<<0;d.beginPath();if(this.scaleBreaks&&!u(this.scaleBreaks.firstBreakIndex))if(u(this.scaleBreaks.lastBreakIndex))e=this.scaleBreaks._appliedBreaks[this.scaleBreaks.firstBreakIndex].endPixel+b;else for(l=this.scaleBreaks.firstBreakIndex;l<=this.scaleBreaks.lastBreakIndex;l++)d.moveTo(k,e),d.lineTo(k,this.scaleBreaks._appliedBreaks[l].startPixel+c), +e=this.scaleBreaks._appliedBreaks[l].endPixel+b;e&&(d.moveTo(k,e),d.lineTo(k,g));d.stroke()}r&&(a.drawImage(this.chart._preRenderCanvas,0,0,this.chart.width,this.chart.height),this.chart._breaksCanvasCtx&&this.chart._breaksCanvasCtx.drawImage(this.chart._preRenderCanvas,0,0,this.chart.width,this.chart.height),d.clearRect(0,0,this.chart.width,this.chart.height));d.restore()};z.prototype.getPixelCoordinatesOnAxis=function(a){var d={};if("bottom"===this._position||"top"===this._position)d.x=this.convertValueToPixel(a), +d.y=this.lineCoordinates.y1;if("left"===this._position||"right"===this._position)d.y=this.convertValueToPixel(a),d.x=this.lineCoordinates.x2;return d};z.prototype.convertPixelToValue=function(a){if("undefined"===typeof a)return null;var d=0,b=0,c,d=!0,e=this.scaleBreaks?this.scaleBreaks._appliedBreaks:[],b="number"===typeof a?a:"left"===this._position||"right"===this._position?a.y:a.x;if(this.logarithmic){a=c=Math.pow(this.logarithmBase,(b-this.conversionParameters.reference)/this.conversionParameters.pixelPerUnit); +if(b<=this.conversionParameters.reference===("left"===this._position||"right"===this._position)!==this.reversed)for(b=0;be[b].startValue/this.conversionParameters.minimum){c/=e[b].startValue/this.conversionParameters.minimum;if(ce[b].startValue/e[b-1].endValue){c/=e[b].startValue/e[b-1].endValue;if(cthis.conversionParameters.minimum))if(d)if(e[b].endValue>this.conversionParameters.minimum){if(1e[b].startValue){a=Math.pow(e[b].endValue/e[b].startValue,Math.log(c)/Math.log(e[b].size));break}else a*=e[b].startValue/this.conversionParameters.minimum*Math.pow(e[b].size,Math.log(e[b].startValue/this.conversionParameters.minimum)/Math.log(e[b].endValue/e[b].startValue))*c,c*=Math.pow(e[b].size,Math.log(this.conversionParameters.minimum/e[b].startValue)/Math.log(e[b].endValue/e[b].startValue));d=!1}else if(c1/e[b].size){a*=Math.pow(e[b].endValue/e[b].startValue,1>=e[b].size?1:Math.log(c)/Math.log(e[b].size))*c;break}else a/=e[b].endValue/e[b].startValue/e[b].size;c*=e[b].size;d=!1}else break;else if(c1/e[b].size){a*=Math.pow(e[b].endValue/e[b].startValue,1>=e[b].size?1:Math.log(c)/Math.log(e[b].size))*c;break}else a/=e[b].endValue/e[b].startValue/e[b].size;c*=e[b].size}else break; +d=a*this.viewportMinimum}else{a=c=(b-this.conversionParameters.reference)/this.conversionParameters.pixelPerUnit;if(b<=this.conversionParameters.reference===("left"===this._position||"right"===this._position)!==this.reversed)for(b=0;b=e[b].size?0:c*(e[b].endValue- +e[b].startValue)/e[b].size;break}else a+=e[b].endValue-this.conversionParameters.minimum-e[b].size*(e[b].endValue-this.conversionParameters.minimum)/(e[b].endValue-e[b].startValue),c-=e[b].size*(e[b].endValue-this.conversionParameters.minimum)/(e[b].endValue-e[b].startValue);d=!1}else if(c>e[b].startValue-this.conversionParameters.minimum){c-=e[b].startValue-this.conversionParameters.minimum;if(ce[b].startValue-e[b-1].endValue){c-=e[b].startValue-e[b-1].endValue;if(cthis.conversionParameters.minimum))if(d)if(e[b].endValue>this.conversionParameters.minimum)if(e[b].size&&this.conversionParameters.minimum+c*(e[b].endValue- +e[b].startValue)/e[b].size>e[b].startValue){a=0>=e[b].size?0:c*(e[b].endValue-e[b].startValue)/e[b].size;break}else a+=e[b].startValue-this.conversionParameters.minimum+e[b].size*(this.conversionParameters.minimum-e[b].startValue)/(e[b].endValue-e[b].startValue),c+=e[b].size*(this.conversionParameters.minimum-e[b].startValue)/(e[b].endValue-e[b].startValue),d=!1;else if(c-1*e[b].size){a+=(e[b].endValue- +e[b].startValue)*(0===e[b].size?1:c/e[b].size)+c;break}else a-=e[b].endValue-e[b].startValue-e[b].size;c+=e[b].size;d=!1}else break;else if(c-1*e[b].size){a+=(e[b].endValue-e[b].startValue)*(0===e[b].size?1:c/e[b].size)+c;break}else a-=e[b].endValue-e[b].startValue-e[b].size;c+=e[b].size}else break;d=this.conversionParameters.minimum+a}return d};z.prototype.convertValueToPixel=function(a){a=this.getApparentDifference(this.conversionParameters.minimum, +a,a);return this.logarithmic?this.conversionParameters.reference+this.conversionParameters.pixelPerUnit*Math.log(a/this.conversionParameters.minimum)/this.conversionParameters.lnLogarithmBase+0.5<<0:"axisX"===this.type?this.conversionParameters.reference+this.conversionParameters.pixelPerUnit*(a-this.conversionParameters.minimum)+0.5<<0:this.conversionParameters.reference+this.conversionParameters.pixelPerUnit*(a-this.conversionParameters.minimum)+0.5};z.prototype.getApparentDifference=function(a, +d,b,c){var e=this.scaleBreaks?this.scaleBreaks._appliedBreaks:[];if(this.logarithmic){b=u(b)?d/a:b;for(var g=0;ge[g].endValue||(a<=e[g].startValue&&d>=e[g].endValue?b=b/e[g].endValue*e[g].startValue*e[g].size:a>=e[g].startValue&&d>=e[g].endValue?b=b/e[g].endValue*a*Math.pow(e[g].size,Math.log(e[g].endValue/a)/Math.log(e[g].endValue/e[g].startValue)):a<=e[g].startValue&&d<=e[g].endValue?b=b/d*e[g].startValue*Math.pow(e[g].size,Math.log(d/e[g].startValue)/Math.log(e[g].endValue/ +e[g].startValue)):!c&&(a>e[g].startValue&&de[g].endValue||(a<=e[g].startValue&&d>=e[g].endValue?b=b-e[g].endValue+e[g].startValue+e[g].size:a>e[g].startValue&&d>=e[g].endValue?b=b-e[g].endValue+a+e[g].size*(e[g].endValue-a)/(e[g].endValue-e[g].startValue):a<=e[g].startValue&&de[g].startValue&&da[e].endValue||(this.viewportMinimum>=a[e].startValue&&this.viewportMaximum<= +a[e].endValue?b=0:this.viewportMinimum<=a[e].startValue&&this.viewportMaximum>=a[e].endValue?(c=c/a[e].endValue*a[e].startValue,b=0a[e].startValue&&this.viewportMaximum>=a[e].endValue?(c=c/a[e].endValue*this.viewportMinimum,b=0a[e].endValue||(this.viewportMinimum>=a[e].startValue&&this.viewportMaximum<=a[e].endValue?b=0:this.viewportMinimum<=a[e].startValue&&this.viewportMaximum>=a[e].endValue?(c=c-a[e].endValue+a[e].startValue,b=0a[e].startValue&&this.viewportMaximum>=a[e].endValue?(c=c-a[e].endValue+this.viewportMinimum, +b=0this.maxWidth?8:6);var a=Math.max(c,Math.floor(this.maxWidth/a)),e,g,k,c=0;!u(this.options.viewportMinimum)&&(!u(this.options.viewportMaximum)&&this.options.viewportMinimum>=this.options.viewportMaximum)&& +(this.viewportMinimum=this.viewportMaximum=null);if(u(this.options.viewportMinimum)&&!u(this.sessionVariables.newViewportMinimum)&&!isNaN(this.sessionVariables.newViewportMinimum))this.viewportMinimum=this.sessionVariables.newViewportMinimum;else if(null===this.viewportMinimum||isNaN(this.viewportMinimum))this.viewportMinimum=this.minimum;if(u(this.options.viewportMaximum)&&!u(this.sessionVariables.newViewportMaximum)&&!isNaN(this.sessionVariables.newViewportMaximum))this.viewportMaximum=this.sessionVariables.newViewportMaximum; +else if(null===this.viewportMaximum||isNaN(this.viewportMaximum))this.viewportMaximum=this.maximum;if(this.scaleBreaks)for(c=0;c=this.scaleBreaks._appliedBreaks[c].startValue||!u(this.options.minimum)&&this.options.minimum>=this.scaleBreaks._appliedBreaks[c].startValue||!u(this.options.viewportMinimum)&&this.viewportMinimum>=this.scaleBreaks._appliedBreaks[c].startValue)&& +(!u(this.sessionVariables.newViewportMaximum)&&this.sessionVariables.newViewportMaximum<=this.scaleBreaks._appliedBreaks[c].endValue||!u(this.options.maximum)&&this.options.maximum<=this.scaleBreaks._appliedBreaks[c].endValue||!u(this.options.viewportMaximum)&&this.viewportMaximum<=this.scaleBreaks._appliedBreaks[c].endValue)){this.scaleBreaks._appliedBreaks.splice(c,1);break}if("axisX"===this.type){if(this.dataSeries&&0g?(c=Math.min(0.01*Math.abs(this.getApparentDifference(g,e,null,!0)),5),0<=g?e=g-c:g=isFinite(e)?e+c:0):(c=Math.min(0.01*Math.abs(this.getApparentDifference(e,g, +null,!0)),0.05),0!==g&&(g+=c),0!==e&&(e-=c)),k=Infinity!==this.dataInfo.minDiff?this.dataInfo.minDiff:1g&&(g=0));c=this.getApparentDifference(isNaN(this.viewportMinimum)||null===this.viewportMinimum?e:this.viewportMinimum,isNaN(this.viewportMaximum)||null===this.viewportMaximum?g:this.viewportMaximum,null, +!0);if("axisX"===this.type&&b){this.intervalType||(c/1<=a?(this.interval=1,this.intervalType="millisecond"):c/2<=a?(this.interval=2,this.intervalType="millisecond"):c/5<=a?(this.interval=5,this.intervalType="millisecond"):c/10<=a?(this.interval=10,this.intervalType="millisecond"):c/20<=a?(this.interval=20,this.intervalType="millisecond"):c/50<=a?(this.interval=50,this.intervalType="millisecond"):c/100<=a?(this.interval=100,this.intervalType="millisecond"):c/200<=a?(this.interval=200,this.intervalType= +"millisecond"):c/250<=a?(this.interval=250,this.intervalType="millisecond"):c/300<=a?(this.interval=300,this.intervalType="millisecond"):c/400<=a?(this.interval=400,this.intervalType="millisecond"):c/500<=a?(this.interval=500,this.intervalType="millisecond"):c/(1*S.secondDuration)<=a?(this.interval=1,this.intervalType="second"):c/(2*S.secondDuration)<=a?(this.interval=2,this.intervalType="second"):c/(5*S.secondDuration)<=a?(this.interval=5,this.intervalType="second"):c/(10*S.secondDuration)<=a?(this.interval= +10,this.intervalType="second"):c/(15*S.secondDuration)<=a?(this.interval=15,this.intervalType="second"):c/(20*S.secondDuration)<=a?(this.interval=20,this.intervalType="second"):c/(30*S.secondDuration)<=a?(this.interval=30,this.intervalType="second"):c/(1*S.minuteDuration)<=a?(this.interval=1,this.intervalType="minute"):c/(2*S.minuteDuration)<=a?(this.interval=2,this.intervalType="minute"):c/(5*S.minuteDuration)<=a?(this.interval=5,this.intervalType="minute"):c/(10*S.minuteDuration)<=a?(this.interval= +10,this.intervalType="minute"):c/(15*S.minuteDuration)<=a?(this.interval=15,this.intervalType="minute"):c/(20*S.minuteDuration)<=a?(this.interval=20,this.intervalType="minute"):c/(30*S.minuteDuration)<=a?(this.interval=30,this.intervalType="minute"):c/(1*S.hourDuration)<=a?(this.interval=1,this.intervalType="hour"):c/(2*S.hourDuration)<=a?(this.interval=2,this.intervalType="hour"):c/(3*S.hourDuration)<=a?(this.interval=3,this.intervalType="hour"):c/(6*S.hourDuration)<=a?(this.interval=6,this.intervalType= +"hour"):c/(1*S.dayDuration)<=a?(this.interval=1,this.intervalType="day"):c/(2*S.dayDuration)<=a?(this.interval=2,this.intervalType="day"):c/(4*S.dayDuration)<=a?(this.interval=4,this.intervalType="day"):c/(1*S.weekDuration)<=a?(this.interval=1,this.intervalType="week"):c/(2*S.weekDuration)<=a?(this.interval=2,this.intervalType="week"):c/(3*S.weekDuration)<=a?(this.interval=3,this.intervalType="week"):c/(1*S.monthDuration)<=a?(this.interval=1,this.intervalType="month"):c/(2*S.monthDuration)<=a?(this.interval= +2,this.intervalType="month"):c/(3*S.monthDuration)<=a?(this.interval=3,this.intervalType="month"):c/(6*S.monthDuration)<=a?(this.interval=6,this.intervalType="month"):(this.interval=c/(1*S.yearDuration)<=a?1:c/(2*S.yearDuration)<=a?2:c/(4*S.yearDuration)<=a?4:Math.floor(z.getNiceNumber(c/(a-1),!0)/S.yearDuration),this.intervalType="year"));if(null===this.viewportMinimum||isNaN(this.viewportMinimum))this.viewportMinimum=e-k/2;if(null===this.viewportMaximum||isNaN(this.viewportMaximum))this.viewportMaximum= +g+k/2;d?this.autoValueFormatString="MMM DD YYYY HH:mm":"year"===this.intervalType?this.autoValueFormatString="YYYY":"month"===this.intervalType?this.autoValueFormatString="MMM YYYY":"week"===this.intervalType?this.autoValueFormatString="MMM DD YYYY":"day"===this.intervalType?this.autoValueFormatString="MMM DD YYYY":"hour"===this.intervalType?this.autoValueFormatString="hh:mm TT":"minute"===this.intervalType?this.autoValueFormatString="hh:mm TT":"second"===this.intervalType?this.autoValueFormatString= +"hh:mm:ss TT":"millisecond"===this.intervalType&&(this.autoValueFormatString="fff'ms'");this.valueFormatString||(this.valueFormatString=this.autoValueFormatString)}else{this.intervalType="number";c=z.getNiceNumber(c,!1);this.interval=this.options&&0g?(c=Math.min(0.01*Math.abs(this.getApparentDifference(g,e,null,!0)),5),0<=g?e=g-c:g=isFinite(e)?e+c:0):(c=Math.min(0.01*Math.abs(this.getApparentDifference(e,g,null,!0)),0.05),0!==g&&(g+=c),0!==e&&(e-=c)):(g="undefined"===typeof this.options.interval?-Infinity:this.options.interval,e="undefined"!==typeof this.options.interval||isFinite(this.dataInfo.minDiff)?0:Infinity),k=Infinity!==this.dataInfo.minDiff?this.dataInfo.minDiff:1g&&(g=0)),Math.abs(this.getApparentDifference(e,g,null,!0)),"axisX"===this.type&&b){this.valueType="dateTime";if(null===this.minimum||isNaN(this.minimum))this.minimum=e-k/2;if(null===this.maximum||isNaN(this.maximum))this.maximum=g+k/2}else this.intervalType=this.valueType="number",null===this.minimum&&(this.minimum="axisX"===this.type?e-k/2:Math.floor(e/this.interval)*this.interval,this.minimum=Math.min(this.minimum, +null===this.sessionVariables.viewportMinimum||isNaN(this.sessionVariables.viewportMinimum)?Infinity:this.sessionVariables.viewportMinimum)),null===this.maximum&&(this.maximum="axisX"===this.type?g+k/2:Math.ceil(g/this.interval)*this.interval,this.maximum=Math.max(this.maximum,null===this.sessionVariables.viewportMaximum||isNaN(this.sessionVariables.viewportMaximum)?-Infinity:this.sessionVariables.viewportMaximum)),0===this.maximum&&0===this.minimum&&(0===this.options.minimum?this.maximum+=10:0=== +this.options.maximum&&(this.minimum-=10));u(this.sessionVariables.newViewportMinimum)&&(this.viewportMinimum=Math.max(this.viewportMinimum,this.minimum));u(this.sessionVariables.newViewportMaximum)&&(this.viewportMaximum=Math.min(this.viewportMaximum,this.maximum));this.range=this.viewportMaximum-this.viewportMinimum;this.intervalStartPosition="axisX"===this.type&&b?this.getLabelStartPoint(new Date(this.viewportMinimum),this.intervalType,this.interval):Math.floor((this.viewportMinimum+0.2*this.interval)/ +this.interval)*this.interval;this.valueFormatString||(this.valueFormatString=z.generateValueFormatString(this.range,2))}};z.prototype.calculateLogarithmicAxisParameters=function(){var a=this.chart.layoutManager.getFreeSpace(),d=Math.log(this.logarithmBase),b;"bottom"===this._position||"top"===this._position?(this.maxWidth=a.width,this.maxHeight=a.height):(this.maxWidth=a.height,this.maxHeight=a.width);var a="axisX"===this.type?500>this.maxWidth?7:Math.max(7,Math.floor(this.maxWidth/100)):Math.max(Math.floor(this.maxWidth/ +50),3),c,e,g,k;k=1;if(null===this.viewportMinimum||isNaN(this.viewportMinimum))this.viewportMinimum=this.minimum;if(null===this.viewportMaximum||isNaN(this.viewportMaximum))this.viewportMaximum=this.maximum;if(this.scaleBreaks)for(k=0;k=this.scaleBreaks._appliedBreaks[k].startValue||!u(this.options.minimum)&&this.options.minimum>=this.scaleBreaks._appliedBreaks[k].startValue|| +!u(this.options.viewportMinimum)&&this.viewportMinimum>=this.scaleBreaks._appliedBreaks[k].startValue)&&(!u(this.sessionVariables.newViewportMaximum)&&this.sessionVariables.newViewportMaximum<=this.scaleBreaks._appliedBreaks[k].endValue||!u(this.options.maximum)&&this.options.maximum<=this.scaleBreaks._appliedBreaks[k].endValue||!u(this.options.viewportMaximum)&&this.viewportMaximum<=this.scaleBreaks._appliedBreaks[k].endValue)){this.scaleBreaks._appliedBreaks.splice(k,1);break}"axisX"===this.type? +(c=null!==this.viewportMinimum?this.viewportMinimum:this.dataInfo.viewPortMin,e=null!==this.viewportMaximum?this.viewportMaximum:this.dataInfo.viewPortMax,1===e/c&&(k=Math.pow(this.logarithmBase,"undefined"===typeof this.options.interval?0.4:this.options.interval),e*=k,c/=k),g=Infinity!==this.dataInfo.minDiff?this.dataInfo.minDiff:e/c>this.logarithmBase?e/c*Math.pow(this.logarithmBase,0.5):this.logarithmBase):"axisY"===this.type&&(c=null!==this.viewportMinimum?this.viewportMinimum:this.dataInfo.viewPortMin, +e=null!==this.viewportMaximum?this.viewportMaximum:this.dataInfo.viewPortMax,0>=c&&!isFinite(e)?(e="undefined"===typeof this.options.interval?0:this.options.interval,c=1):0>=c?c=e:isFinite(e)||(e=c),1===c&&1===e?(e*=this.logarithmBase-1/this.logarithmBase,c=1):1===e/c?(k=Math.min(e*Math.pow(this.logarithmBase,0.01),Math.pow(this.logarithmBase,5)),e*=k,c/=k):c>e?(k=Math.min(c/e*Math.pow(this.logarithmBase,0.01),Math.pow(this.logarithmBase,5)),1<=e?c=e/k:e=c*k):(k=Math.min(e/c*Math.pow(this.logarithmBase, +0.01),Math.pow(this.logarithmBase,0.04)),1!==e&&(e*=k),1!==c&&(c/=k)),g=Infinity!==this.dataInfo.minDiff?this.dataInfo.minDiff:e/c>this.logarithmBase?e/c*Math.pow(this.logarithmBase,0.5):this.logarithmBase,this.includeZero&&(null===this.viewportMinimum||isNaN(this.viewportMinimum))&&1e&&(e=1));k=(isNaN(this.viewportMaximum)||null===this.viewportMaximum?e:this.viewportMaximum)/(isNaN(this.viewportMinimum)||null=== +this.viewportMinimum?c:this.viewportMinimum);var l=(isNaN(this.viewportMaximum)||null===this.viewportMaximum?e:this.viewportMaximum)-(isNaN(this.viewportMinimum)||null===this.viewportMinimum?c:this.viewportMinimum);this.intervalType="number";k=Math.pow(this.logarithmBase,z.getNiceNumber(Math.abs(Math.log(k)/d),!1));this.options&&0this.logarithmBase?e/c*Math.pow(this.logarithmBase,0.5):this.logarithmBase):"axisY"===this.type&&(c=null!==this.minimum?this.minimum:this.dataInfo.min,e=null!==this.maximum?this.maximum:this.dataInfo.max,isFinite(c)||isFinite(e)?1===c&&1===e?(e*=this.logarithmBase,c/=this.logarithmBase):1===e/c?(k=Math.pow(this.logarithmBase,this.interval),e*=k,c/=k):c>e?(k= +Math.min(0.01*(c/e),5),1<=e?c=e/k:e=c*k):(k=Math.min(e/c*Math.pow(this.logarithmBase,0.01),Math.pow(this.logarithmBase,0.04)),1!==e&&(e*=k),1!==c&&(c/=k)):(e="undefined"===typeof this.options.interval?0:this.options.interval,c=1),g=Infinity!==this.dataInfo.minDiff?this.dataInfo.minDiff:e/c>this.logarithmBase?e/c*Math.pow(this.logarithmBase,0.5):this.logarithmBase,this.includeZero&&(null===this.minimum||isNaN(this.minimum))&&1e&&(e=1)),this.intervalType="number",null===this.minimum&&(this.minimum="axisX"===this.type?c/Math.sqrt(g):Math.pow(this.logarithmBase,this.interval*Math.floor(Math.log(c)/d/this.interval)),this.minimum=Math.min(this.minimum,null===this.sessionVariables.viewportMinimum||isNaN(this.sessionVariables.viewportMinimum)?"undefined"===typeof this.sessionVariables.newViewportMinimum?Infinity:this.sessionVariables.newViewportMinimum:this.sessionVariables.viewportMinimum)),null===this.maximum&&(this.maximum= +"axisX"===this.type?e*Math.sqrt(g):Math.pow(this.logarithmBase,this.interval*Math.ceil(Math.log(e)/d/this.interval)),this.maximum=Math.max(this.maximum,null===this.sessionVariables.viewportMaximum||isNaN(this.sessionVariables.viewportMaximum)?"undefined"===typeof this.sessionVariables.newViewportMaximum?0:this.sessionVariables.newViewportMaximum:this.sessionVariables.viewportMaximum)),1===this.maximum&&1===this.minimum&&(1===this.options.minimum?this.maximum*=this.logarithmBase-1/this.logarithmBase: +1===this.options.maximum&&(this.minimum/=this.logarithmBase-1/this.logarithmBase));this.viewportMinimum=Math.max(this.viewportMinimum,this.minimum);this.viewportMaximum=Math.min(this.viewportMaximum,this.maximum);this.viewportMinimum>this.viewportMaximum&&(!this.options.viewportMinimum&&!this.options.minimum||this.options.viewportMaximum||this.options.maximum?this.options.viewportMinimum||this.options.minimum||!this.options.viewportMaximum&&!this.options.maximum||(this.viewportMinimum=this.minimum= +(this.options.viewportMaximum||this.options.maximum)/Math.pow(this.logarithmBase,2*Math.ceil(this.interval))):this.viewportMaximum=this.maximum=this.options.viewportMinimum||this.options.minimum);c=Math.pow(this.logarithmBase,Math.floor(Math.log(this.viewportMinimum)/(d*this.interval)+0.2)*this.interval);this.range=this.viewportMaximum/this.viewportMinimum;this.noTicks=a;if(!this.options.interval&&this.rangethis.viewportMaximum||3>a?2:3)){for(d=Math.floor(this.viewportMinimum/ +b+0.5)*b;dthis.interval&&(this.interval=b,c=Math.pow(this.logarithmBase,Math.floor(Math.log(this.viewportMinimum)/(d*this.interval)+0.2)*this.interval))),this.equidistantInterval=!0,this.intervalStartPosition=c;if(!this.valueFormatString&&(this.valueFormatString="#,##0.##",1>this.viewportMinimum)){d=Math.floor(Math.abs(Math.log(this.viewportMinimum)/ +Math.LN10))+2;if(isNaN(d)||!isFinite(d))d=2;if(2a&&(c+=Math.floor(Math.abs(Math.log(a)/Math.LN10)),isNaN(c)||!isFinite(c))&&(c=d);for(var e=0;eb?1>=c?1:5>=c?5:10:Math.max(Math.floor(c),1);return-20>b?Number(c*Math.pow(10,b)):Number((c*Math.pow(10,b)).toFixed(20))};z.getNiceNumber= +function(a,d){var b=Math.floor(Math.log(a)/Math.LN10),c=a/Math.pow(10,b),c=d?1.5>c?1:3>c?2:7>c?5:10:1>=c?1:2>=c?2:5>=c?5:10;return-20>b?Number(c*Math.pow(10,b)):Number((c*Math.pow(10,b)).toFixed(20))};z.prototype.getLabelStartPoint=function(){var a=S[this.intervalType+"Duration"]*this.interval,a=new Date(Math.floor(this.viewportMinimum/a)*a);if("millisecond"!==this.intervalType)if("second"===this.intervalType)0=a||"bottom"===this.scaleBreaks.parent._position&&0<=a)this.ctx.lineTo(c,l),this.ctx.lineTo(k,l),this.ctx.lineTo(k,e);else if("wavy"===this.type){h=c;m=e;g=0.5;p=(l-m)/a/3;for(var n=0;n=a||"right"===this.scaleBreaks.parent._position&&0<=a)this.ctx.lineTo(k,e),this.ctx.lineTo(k,l), +this.ctx.lineTo(c,l);else if("wavy"===this.type){h=c;m=e;g=0.5;p=(k-h)/a/3;for(n=0;nthis.parent.range?2:Math.floor(Math.abs(Math.log(this.parent.range)/Math.LN10))+(5>this.parent.range?2:10>this.parent.range? +1:0):50this.parent.range?2:10>this.parent.range?1:0);this.valueFormatString=z.generateValueFormatString(this.parent.range,m)}var l=null===this.opacity?1:this.opacity,m=Math.abs("pixel"===this._thicknessType?this.thickness:this.parent.conversionParameters.pixelPerUnit*this.thickness),p=this.chart.overlaidCanvasCtx,q=p.globalAlpha;p.globalAlpha=l;p.beginPath();p.strokeStyle=this.color;p.lineWidth=m;p.save();this.labelFontSize= +u(this.options.labelFontSize)?this.parent.labelFontSize:this.labelFontSize;if("left"===this.parent._position||"right"===this.parent._position)this.labelMaxWidth=u(this.options.labelMaxWidth)?this.parent.bounds.x2-this.parent.bounds.x1:this.labelMaxWidth,this.labelMaxHeight=u(this.options.labelWrap)||this.labelWrap?3*this.chart.height:2*this.labelFontSize;else if("top"===this.parent._position||"bottom"===this.parent._position)this.labelMaxWidth=u(this.options.labelMaxWidth)?3*this.chart.width:this.labelMaxWidth, +this.labelMaxHeight=u(this.options.labelWrap)||this.labelWrap?this.parent.bounds.height:2*this.labelFontSize;0this.chart.bounds.x2?l.x=this.chart.bounds.x2-l.width:l.xthis.chart.bounds.y2&&(l.y=this.chart.bounds.y2-l.measureText().height+l.fontSize/2);"left"===this.parent._position?l.x=this.parent.lineCoordinates.x2-l.measureText().width:"right"===this.parent._position&&(l.x=this.parent.lineCoordinates.x2)}}else if("bottom"===this.parent._position||"top"===this.parent._position){n=this.parent.convertPixelToValue({x:a});for(r=0;rthis.chart.bounds.x2&&(l.x=this.chart.bounds.x2-l.width);l.xthis.chart.bounds.y2&&(l.y=this.chart.bounds.y2-l.measureText().height+l.fontSize/2);"left"===this.parent._position?l.x=this.parent.lineCoordinates.x2-l.measureText().width:"right"=== +this.parent._position&&(l.x=this.parent.lineCoordinates.x2)}k=null;("bottom"===this.parent._position||"top"===this.parent._position)&&(b>=this.parent.convertValueToPixel(this.parent.viewportMinimum)&&c<=this.parent.convertValueToPixel(this.parent.viewportMaximum))&&(0=this.parent.convertValueToPixel(this.parent.viewportMaximum)&& +e<=this.parent.convertValueToPixel(this.parent.viewportMinimum))&&(0this.chart.bounds.y2&&(l.y=this.chart.bounds.y2-l.measureText().height+l.fontSize/2);"left"===this.parent._position?l.x=this.parent.lineCoordinates.x1-l.measureText().width:"right"===this.parent._position&&(l.x=this.parent.lineCoordinates.x2)}else{if("bottom"===this.parent._position||"top"===this.parent._position)l.text=this.labelFormatter?this.labelFormatter({chart:this.chart,axis:this.parent.options,crosshair:this.options,value:this.parent.convertPixelToValue(a)}):u(this.options.label)? +ba(this.parent.convertPixelToValue(a),this.valueFormatString,this.chart._cultureInfo):this.label,l.x=b-l.measureText().width/2,l.x+l.width>this.chart.bounds.x2&&(l.x=this.chart.bounds.x2-l.width),l.xthis.chart.bounds.x2&&(l.x=this.chart.bounds.x2-l.width);l.xthis.chart.bounds.y2&&(l.y=this.chart.bounds.y2-l.measureText().height+l.fontSize/2),"left"===this.parent._position?l.x=this.parent.lineCoordinates.x2-l.measureText().width:"right"===this.parent._position&&(l.x=this.parent.lineCoordinates.x2);0(new Date).getTime()-this._lastUpdated||(this._lastUpdated=(new Date).getTime(), +this.chart.resetOverlayedCanvas(),this._updateToolTip(a,d))};$.prototype._updateToolTip=function(a,d,b){b="undefined"===typeof b?!0:b;this.container||this._initialize();this.enabled||this.hide();if(!this.chart.disableToolTip){if("undefined"===typeof a||"undefined"===typeof d){if(isNaN(this._prevX)||isNaN(this._prevY))return;a=this._prevX;d=this._prevY}else this._prevX=a,this._prevY=d;var c=null,e=null,g=[],k=0;if(this.shared&&this.enabled&&"none"!==this.chart.plotInfo.axisPlacement){if("xySwapped"=== +this.chart.plotInfo.axisPlacement){var l=[];if(this.chart.axisX)for(var h=0;hm.dataSeries.axisY.viewportMaximum&&b++;b-m.dataPoint.y.length&&g.push(m)}else"column"===e.type||"bar"===e.type?0>m.dataPoint.y?0>m.dataSeries.axisY.viewportMinimum&&m.dataSeries.axisY.viewportMaximum>=m.dataPoint.y&&g.push(m):m.dataSeries.axisY.viewportMinimum<=m.dataPoint.y&&0<=m.dataSeries.axisY.viewportMaximum&&g.push(m):"bubble"===e.type?(b=this.chart._eventManager.objectMap[e.dataPointIds[m.index]].size/2,m.dataPoint.y>= +m.dataSeries.axisY.viewportMinimum-b&&m.dataPoint.y<=m.dataSeries.axisY.viewportMaximum+b&&g.push(m)):"waterfall"===e.type?(b=0,m.cumulativeSumYStartValuem.dataSeries.axisY.viewportMaximum&&b++,m.cumulativeSumm.dataSeries.axisY.viewportMaximum&&b++,2>b&&-2=m.dataSeries.axisY.viewportMinimum&& +m.dataPoint.y<=m.dataSeries.axisY.viewportMaximum)&&g.push(m);else g.push(m)}}if(0a&&(a+=this.container.clientWidth+20);a+this.container.clientWidth> +Math.max(this.chart.container.clientWidth,this.chart.width)&&(a=Math.max(0,Math.max(this.chart.container.clientWidth,this.chart.width)-this.container.clientWidth));d=1!==g.length||this.shared||"line"!==g[0].dataSeries.type&&"stepLine"!==g[0].dataSeries.type&&"spline"!==g[0].dataSeries.type&&"area"!==g[0].dataSeries.type&&"stepArea"!==g[0].dataSeries.type&&"splineArea"!==g[0].dataSeries.type?"bar"===g[0].dataSeries.type||"rangeBar"===g[0].dataSeries.type||"stackedBar"===g[0].dataSeries.type||"stackedBar100"=== +g[0].dataSeries.type?g[0].dataSeries.axisX.convertValueToPixel(g[0].dataPoint.x):d:g[0].dataSeries.axisY.convertValueToPixel(g[0].dataPoint.y);d=-d+10;0":"X:{axisXIndex}
":""),g+=c.toolTipContent?c.toolTipContent:b.toolTipContent?b.toolTipContent:this.content&&"function"!==typeof this.content? +this.content:"{name}:  {y}",p=b.axisXIndex):"bubble"===b.type?(this.chart.axisX&&1":"X:{axisXIndex}
":""),g+=c.toolTipContent?c.toolTipContent:b.toolTipContent?b.toolTipContent:this.content&&"function"!==typeof this.content?this.content:"{name}:  {y},   {z}"): +"rangeColumn"===b.type||"rangeBar"===b.type||"rangeArea"===b.type||"rangeSplineArea"===b.type||"error"===b.type?(this.chart.axisX&&1":"X:{axisXIndex}
":""),g+=c.toolTipContent?c.toolTipContent:b.toolTipContent?b.toolTipContent:this.content&&"function"!==typeof this.content?this.content:"{name}:  {y[0]}, {y[1]}"):"candlestick"=== +b.type||"ohlc"===b.type?(this.chart.axisX&&1":"X:{axisXIndex}
":""),g+=c.toolTipContent?c.toolTipContent:b.toolTipContent?b.toolTipContent:this.content&&"function"!==typeof this.content?this.content:"{name}:
Open:   {y[0]}
High:    {y[1]}
Low:   {y[2]}
Close:   {y[3]}"):"boxAndWhisker"=== +b.type&&(this.chart.axisX&&1":"X:{axisXIndex}
":""),g+=c.toolTipContent?c.toolTipContent:b.toolTipContent?b.toolTipContent:this.content&&"function"!==typeof this.content?this.content:"{name}:
Minimum:   {y[0]}
Q1:               {y[1]}
Q2:               {y[4]}
Q3:               {y[2]}
Maximum:  {y[3]}"), +null===d&&(d=""),!0===this.reversed?(d=this.chart.replaceKeywordsWithValue(g,c,b,e)+d,h"+d)):(d+=this.chart.replaceKeywordsWithValue(g,c,b,e),h")));null!==d&&(d=m+d)}else{b=a[0].dataSeries;c=a[0].dataPoint;e=a[0].index;if(null===c.toolTipContent||"undefined"===typeof c.toolTipContent&&null===b.options.toolTipContent)return null;"line"===b.type||"stepLine"===b.type||"spline"===b.type||"area"===b.type||"stepArea"===b.type||"splineArea"===b.type||"column"=== +b.type||"bar"===b.type||"scatter"===b.type||"stackedColumn"===b.type||"stackedColumn100"===b.type||"stackedBar"===b.type||"stackedBar100"===b.type||"stackedArea"===b.type||"stackedArea100"===b.type||"waterfall"===b.type?g=c.toolTipContent?c.toolTipContent:b.toolTipContent?b.toolTipContent:this.content&&"function"!==typeof this.content?this.content:""+(c.label?"{label}":"{x}")+":  {y}":"bubble"===b.type?g=c.toolTipContent? +c.toolTipContent:b.toolTipContent?b.toolTipContent:this.content&&"function"!==typeof this.content?this.content:""+(c.label?"{label}":"{x}")+":  {y},   {z}":"pie"===b.type||"doughnut"===b.type||"funnel"===b.type||"pyramid"===b.type?g=c.toolTipContent?c.toolTipContent:b.toolTipContent?b.toolTipContent:this.content&&"function"!==typeof this.content?this.content:""+(c.name?"{name}:  ":c.label?"{label}:  ":"")+"{y}":"rangeColumn"===b.type||"rangeBar"===b.type||"rangeArea"===b.type||"rangeSplineArea"===b.type||"error"===b.type?g=c.toolTipContent?c.toolTipContent:b.toolTipContent?b.toolTipContent:this.content&&"function"!==typeof this.content?this.content:""+(c.label?"{label}":"{x}")+" :  {y[0]},  {y[1]}": +"candlestick"===b.type||"ohlc"===b.type?g=c.toolTipContent?c.toolTipContent:b.toolTipContent?b.toolTipContent:this.content&&"function"!==typeof this.content?this.content:""+(c.label?"{label}":"{x}")+"
Open:   {y[0]}
High:    {y[1]}
Low:     {y[2]}
Close:   {y[3]}":"boxAndWhisker"===b.type&&(g=c.toolTipContent?c.toolTipContent:b.toolTipContent?b.toolTipContent: +this.content&&"function"!==typeof this.content?this.content:""+(c.label?"{label}":"{x}")+"
Minimum:   {y[0]}
Q1:               {y[1]}
Q2:               {y[4]}
Q3:               {y[2]}
Maximum:  {y[3]}"); +null===d&&(d="");d+=this.chart.replaceKeywordsWithValue(g,c,b,e)}return d};$.prototype.enableAnimation=function(){if(!this.container.style.WebkitTransition){var a=this.getContainerTransition(this.containerTransitionDuration);this.container.style.WebkitTransition=a;this.container.style.MsTransition=a;this.container.style.transition=a;this.container.style.MozTransition=this.mozContainerTransition}};$.prototype.disableAnimation=function(){this.container.style.WebkitTransition&&(this.container.style.WebkitTransition= +"",this.container.style.MozTransition="",this.container.style.MsTransition="",this.container.style.transition="")};$.prototype.hide=function(a){this.container&&(this.container.style.display="none",this.currentSeriesIndex=-1,this._prevY=this._prevX=NaN,("undefined"===typeof a||a)&&this.chart.resetOverlayedCanvas())};$.prototype.show=function(a,d,b){this._updateToolTip(a,d,"undefined"===typeof b?!1:b)};$.prototype.fixMozTransitionDelay=function(a,d){if(20c&&a.push(d),d.animationCallback(c),1<=c&&d.onComplete)d.onComplete();this.animations=a;0g;g++)for(var e=0;3>e;e++){for(var f=0,d=0;3>d;d++)f+=a[g][d]*b[d][e];c[g][e]=f}return c}function P(a,b){b.fillStyle=a.fillStyle;b.lineCap=a.lineCap;b.lineJoin=a.lineJoin;b.lineWidth=a.lineWidth;b.miterLimit=a.miterLimit;b.shadowBlur=a.shadowBlur;b.shadowColor=a.shadowColor;b.shadowOffsetX= +a.shadowOffsetX;b.shadowOffsetY=a.shadowOffsetY;b.strokeStyle=a.strokeStyle;b.globalAlpha=a.globalAlpha;b.font=a.font;b.textAlign=a.textAlign;b.textBaseline=a.textBaseline;b.arcScaleX_=a.arcScaleX_;b.arcScaleY_=a.arcScaleY_;b.lineScale_=a.lineScale_}function Q(a){var b=a.indexOf("(",3),c=a.indexOf(")",b+1),b=a.substring(b+1,c).split(",");if(4!=b.length||"a"!=a.charAt(3))b[3]=1;return b}function E(a,b,c){return Math.min(c,Math.max(b,a))}function F(a,b,c){0>c&&c++;16*c?a+6*(b-a)*c: +1>2*c?b:2>3*c?a+6*(b-a)*(2/3-c):a}function G(a){if(a in H)return H[a];var b,c=1;a=String(a);if("#"==a.charAt(0))b=a;else if(/^rgb/.test(a)){c=Q(a);b="#";for(var g,e=0;3>e;e++)g=-1!=c[e].indexOf("%")?Math.floor(255*(parseFloat(c[e])/100)):+c[e],b+=v[E(g,0,255)];c=+c[3]}else if(/^hsl/.test(a)){e=c=Q(a);b=parseFloat(e[0])/360%360;0>b&&b++;g=E(parseFloat(e[1])/100,0,1);e=E(parseFloat(e[2])/100,0,1);if(0==g)g=e=b=e;else{var f=0.5>e?e*(1+g):e+g-e*g,d=2*e-f;g=F(d,f,b+1/3);e=F(d,f,b);b=F(d,f,b-1/3)}b="#"+ +v[Math.floor(255*g)]+v[Math.floor(255*e)]+v[Math.floor(255*b)];c=c[3]}else b=Z[a]||a;return H[a]={color:b,alpha:c}}function C(a){this.m_=D();this.mStack_=[];this.aStack_=[];this.currentPath_=[];this.fillStyle=this.strokeStyle="#000";this.lineWidth=1;this.lineJoin="miter";this.lineCap="butt";this.miterLimit=1*q;this.globalAlpha=1;this.font="10px sans-serif";this.textAlign="left";this.textBaseline="alphabetic";this.canvas=a;var b="width:"+a.clientWidth+"px;height:"+a.clientHeight+"px;overflow:hidden;position:absolute", +c=a.ownerDocument.createElement("div");c.style.cssText=b;a.appendChild(c);b=c.cloneNode(!1);b.style.backgroundColor="red";b.style.filter="alpha(opacity=0)";a.appendChild(b);this.element_=c;this.lineScale_=this.arcScaleY_=this.arcScaleX_=1}function R(a,b,c,g){a.currentPath_.push({type:"bezierCurveTo",cp1x:b.x,cp1y:b.y,cp2x:c.x,cp2y:c.y,x:g.x,y:g.y});a.currentX_=g.x;a.currentY_=g.y}function S(a,b){var c=G(a.strokeStyle),g=c.color,c=c.alpha*a.globalAlpha,e=a.lineScale_*a.lineWidth;1>e&&(c*=e);b.push("')}function T(a,b,c,g){var e=a.fillStyle,f=a.arcScaleX_,d=a.arcScaleY_,k=g.x-c.x,n=g.y-c.y;if(e instanceof w){var h=0,l=g=0,u=0,m=1;if("gradient"==e.type_){h=e.x1_/f;c=e.y1_/d;var p=s(a,e.x0_/f,e.y0_/d),h=s(a,h,c),h=180*Math.atan2(h.x-p.x,h.y-p.y)/Math.PI;0>h&&(h+=360);1E-6>h&&(h=0)}else p=s(a,e.x0_,e.y0_),g=(p.x-c.x)/k,l=(p.y-c.y)/n,k/=f*q, +n/=d*q,m=x.max(k,n),u=2*e.r0_/m,m=2*e.r1_/m-u;f=e.colors_;f.sort(function(a,b){return a.offset-b.offset});d=f.length;p=f[0].color;c=f[d-1].color;k=f[0].alpha*a.globalAlpha;a=f[d-1].alpha*a.globalAlpha;for(var n=[],r=0;r')}else e instanceof +I?k&&n&&b.push("'):(e=G(a.fillStyle),b.push(''))}function s(a,b,c){a=a.m_;return{x:q*(b*a[0][0]+c*a[1][0]+a[2][0])-r,y:q*(b*a[0][1]+c*a[1][1]+a[2][1])-r}}function z(a,b,c){isFinite(b[0][0])&&(isFinite(b[0][1])&&isFinite(b[1][0])&&isFinite(b[1][1])&&isFinite(b[2][0])&&isFinite(b[2][1]))&&(a.m_=b,c&&(a.lineScale_=aa(ba(b[0][0]*b[1][1]-b[0][1]* +b[1][0]))))}function w(a){this.type_=a;this.r1_=this.y1_=this.x1_=this.r0_=this.y0_=this.x0_=0;this.colors_=[]}function I(a,b){if(!a||1!=a.nodeType||"IMG"!=a.tagName)throw new A("TYPE_MISMATCH_ERR");if("complete"!=a.readyState)throw new A("INVALID_STATE_ERR");switch(b){case "repeat":case null:case "":this.repetition_="repeat";break;case "repeat-x":case "repeat-y":case "no-repeat":this.repetition_=b;break;default:throw new A("SYNTAX_ERR");}this.src_=a.src;this.width_=a.width;this.height_=a.height} +function A(a){this.code=this[a];this.message=a+": DOM Exception "+this.code}var x=Math,k=x.round,J=x.sin,K=x.cos,ba=x.abs,aa=x.sqrt,q=10,r=q/2;navigator.userAgent.match(/MSIE ([\d.]+)?/);var M=Array.prototype.slice;O(document);var U={init:function(a){a=a||document;a.createElement("canvas");a.attachEvent("onreadystatechange",W(this.init_,this,a))},init_:function(a){a=a.getElementsByTagName("canvas");for(var b=0;bd;d++)for(var B=0;16>B;B++)v[16*d+B]=d.toString(16)+B.toString(16);var Z={aliceblue:"#F0F8FF",antiquewhite:"#FAEBD7",aquamarine:"#7FFFD4",azure:"#F0FFFF",beige:"#F5F5DC", +bisque:"#FFE4C4",black:"#000000",blanchedalmond:"#FFEBCD",blueviolet:"#8A2BE2",brown:"#A52A2A",burlywood:"#DEB887",cadetblue:"#5F9EA0",chartreuse:"#7FFF00",chocolate:"#D2691E",coral:"#FF7F50",cornflowerblue:"#6495ED",cornsilk:"#FFF8DC",crimson:"#DC143C",cyan:"#00FFFF",darkblue:"#00008B",darkcyan:"#008B8B",darkgoldenrod:"#B8860B",darkgray:"#A9A9A9",darkgreen:"#006400",darkgrey:"#A9A9A9",darkkhaki:"#BDB76B",darkmagenta:"#8B008B",darkolivegreen:"#556B2F",darkorange:"#FF8C00",darkorchid:"#9932CC",darkred:"#8B0000", +darksalmon:"#E9967A",darkseagreen:"#8FBC8F",darkslateblue:"#483D8B",darkslategray:"#2F4F4F",darkslategrey:"#2F4F4F",darkturquoise:"#00CED1",darkviolet:"#9400D3",deeppink:"#FF1493",deepskyblue:"#00BFFF",dimgray:"#696969",dimgrey:"#696969",dodgerblue:"#1E90FF",firebrick:"#B22222",floralwhite:"#FFFAF0",forestgreen:"#228B22",gainsboro:"#DCDCDC",ghostwhite:"#F8F8FF",gold:"#FFD700",goldenrod:"#DAA520",grey:"#808080",greenyellow:"#ADFF2F",honeydew:"#F0FFF0",hotpink:"#FF69B4",indianred:"#CD5C5C",indigo:"#4B0082", +ivory:"#FFFFF0",khaki:"#F0E68C",lavender:"#E6E6FA",lavenderblush:"#FFF0F5",lawngreen:"#7CFC00",lemonchiffon:"#FFFACD",lightblue:"#ADD8E6",lightcoral:"#F08080",lightcyan:"#E0FFFF",lightgoldenrodyellow:"#FAFAD2",lightgreen:"#90EE90",lightgrey:"#D3D3D3",lightpink:"#FFB6C1",lightsalmon:"#FFA07A",lightseagreen:"#20B2AA",lightskyblue:"#87CEFA",lightslategray:"#778899",lightslategrey:"#778899",lightsteelblue:"#B0C4DE",lightyellow:"#FFFFE0",limegreen:"#32CD32",linen:"#FAF0E6",magenta:"#FF00FF",mediumaquamarine:"#66CDAA", +mediumblue:"#0000CD",mediumorchid:"#BA55D3",mediumpurple:"#9370DB",mediumseagreen:"#3CB371",mediumslateblue:"#7B68EE",mediumspringgreen:"#00FA9A",mediumturquoise:"#48D1CC",mediumvioletred:"#C71585",midnightblue:"#191970",mintcream:"#F5FFFA",mistyrose:"#FFE4E1",moccasin:"#FFE4B5",navajowhite:"#FFDEAD",oldlace:"#FDF5E6",olivedrab:"#6B8E23",orange:"#FFA500",orangered:"#FF4500",orchid:"#DA70D6",palegoldenrod:"#EEE8AA",palegreen:"#98FB98",paleturquoise:"#AFEEEE",palevioletred:"#DB7093",papayawhip:"#FFEFD5", +peachpuff:"#FFDAB9",peru:"#CD853F",pink:"#FFC0CB",plum:"#DDA0DD",powderblue:"#B0E0E6",rosybrown:"#BC8F8F",royalblue:"#4169E1",saddlebrown:"#8B4513",salmon:"#FA8072",sandybrown:"#F4A460",seagreen:"#2E8B57",seashell:"#FFF5EE",sienna:"#A0522D",skyblue:"#87CEEB",slateblue:"#6A5ACD",slategray:"#708090",slategrey:"#708090",snow:"#FFFAFA",springgreen:"#00FF7F",steelblue:"#4682B4",tan:"#D2B48C",thistle:"#D8BFD8",tomato:"#FF6347",turquoise:"#40E0D0",violet:"#EE82EE",wheat:"#F5DEB3",whitesmoke:"#F5F5F5",yellowgreen:"#9ACD32"}, +H={},L={},$={butt:"flat",round:"round"},d=C.prototype;d.clearRect=function(){this.textMeasureEl_&&(this.textMeasureEl_.removeNode(!0),this.textMeasureEl_=null);this.element_.innerHTML=""};d.beginPath=function(){this.currentPath_=[]};d.moveTo=function(a,b){var c=s(this,a,b);this.currentPath_.push({type:"moveTo",x:c.x,y:c.y});this.currentX_=c.x;this.currentY_=c.y};d.lineTo=function(a,b){var c=s(this,a,b);this.currentPath_.push({type:"lineTo",x:c.x,y:c.y});this.currentX_=c.x;this.currentY_=c.y};d.bezierCurveTo= +function(a,b,c,g,e,f){e=s(this,e,f);a=s(this,a,b);c=s(this,c,g);R(this,a,c,e)};d.quadraticCurveTo=function(a,b,c,g){a=s(this,a,b);c=s(this,c,g);g={x:this.currentX_+2/3*(a.x-this.currentX_),y:this.currentY_+2/3*(a.y-this.currentY_)};R(this,g,{x:g.x+(c.x-this.currentX_)/3,y:g.y+(c.y-this.currentY_)/3},c)};d.arc=function(a,b,c,g,e,f){c*=q;var d=f?"at":"wa",k=a+K(g)*c-r,n=b+J(g)*c-r;g=a+K(e)*c-r;e=b+J(e)*c-r;k!=g||f||(k+=0.125);a=s(this,a,b);k=s(this,k,n);g=s(this,g,e);this.currentPath_.push({type:d, +x:a.x,y:a.y,radius:c,xStart:k.x,yStart:k.y,xEnd:g.x,yEnd:g.y})};d.rect=function(a,b,c,g){this.moveTo(a,b);this.lineTo(a+c,b);this.lineTo(a+c,b+g);this.lineTo(a,b+g);this.closePath()};d.strokeRect=function(a,b,c,g){var e=this.currentPath_;this.beginPath();this.moveTo(a,b);this.lineTo(a+c,b);this.lineTo(a+c,b+g);this.lineTo(a,b+g);this.closePath();this.stroke();this.currentPath_=e};d.fillRect=function(a,b,c,g){var e=this.currentPath_;this.beginPath();this.moveTo(a,b);this.lineTo(a+c,b);this.lineTo(a+ +c,b+g);this.lineTo(a,b+g);this.closePath();this.fill();this.currentPath_=e};d.createLinearGradient=function(a,b,c,g){var e=new w("gradient");e.x0_=a;e.y0_=b;e.x1_=c;e.y1_=g;return e};d.createRadialGradient=function(a,b,c,g,e,f){var d=new w("gradientradial");d.x0_=a;d.y0_=b;d.r0_=c;d.x1_=g;d.y1_=e;d.r1_=f;return d};d.drawImage=function(a,b){var c,g,e,d,r,y,n,h;e=a.runtimeStyle.width;d=a.runtimeStyle.height;a.runtimeStyle.width="auto";a.runtimeStyle.height="auto";var l=a.width,u=a.height;a.runtimeStyle.width= +e;a.runtimeStyle.height=d;if(3==arguments.length)c=arguments[1],g=arguments[2],r=y=0,n=e=l,h=d=u;else if(5==arguments.length)c=arguments[1],g=arguments[2],e=arguments[3],d=arguments[4],r=y=0,n=l,h=u;else if(9==arguments.length)r=arguments[1],y=arguments[2],n=arguments[3],h=arguments[4],c=arguments[5],g=arguments[6],e=arguments[7],d=arguments[8];else throw Error("Invalid number of arguments");var m=s(this,c,g),p=[];p.push(" ','","");this.element_.insertAdjacentHTML("BeforeEnd",p.join(""))};d.stroke=function(a){var b=[];b.push("d.x)d.x=f.x;if(null==c.y||f.yd.y)d.y=f.y}}b.push(' ">');a?T(this,b,c,d):S(this,b);b.push("");this.element_.insertAdjacentHTML("beforeEnd",b.join(""))};d.fill=function(){this.stroke(!0)};d.closePath=function(){this.currentPath_.push({type:"close"})};d.save=function(){var a= +{};P(this,a);this.aStack_.push(a);this.mStack_.push(this.m_);this.m_=t(D(),this.m_)};d.restore=function(){this.aStack_.length&&(P(this.aStack_.pop(),this),this.m_=this.mStack_.pop())};d.translate=function(a,b){z(this,t([[1,0,0],[0,1,0],[a,b,1]],this.m_),!1)};d.rotate=function(a){var b=K(a);a=J(a);z(this,t([[b,a,0],[-a,b,0],[0,0,1]],this.m_),!1)};d.scale=function(a,b){this.arcScaleX_*=a;this.arcScaleY_*=b;z(this,t([[a,0,0],[0,b,0],[0,0,1]],this.m_),!0)};d.transform=function(a,b,c,d,e,f){z(this,t([[a, +b,0],[c,d,0],[e,f,1]],this.m_),!0)};d.setTransform=function(a,b,c,d,e,f){z(this,[[a,b,0],[c,d,0],[e,f,1]],!0)};d.drawText_=function(a,b,c,d,e){var f=this.m_;d=0;var r=1E3,t=0,n=[],h;h=this.font;if(L[h])h=L[h];else{var l=document.createElement("div").style;try{l.font=h}catch(u){}h=L[h]={style:l.fontStyle||"normal",variant:l.fontVariant||"normal",weight:l.fontWeight||"normal",size:l.fontSize||10,family:l.fontFamily||"sans-serif"}}var l=h,m=this.element_;h={};for(var p in l)h[p]=l[p];p=parseFloat(m.currentStyle.fontSize); +m=parseFloat(l.size);"number"==typeof l.size?h.size=l.size:-1!=l.size.indexOf("px")?h.size=m:-1!=l.size.indexOf("em")?h.size=p*m:-1!=l.size.indexOf("%")?h.size=p/100*m:-1!=l.size.indexOf("pt")?h.size=m/0.75:h.size=p;h.size*=0.981;p=h.style+" "+h.variant+" "+h.weight+" "+h.size+"px "+h.family;m=this.element_.currentStyle;l=this.textAlign.toLowerCase();switch(l){case "left":case "center":case "right":break;case "end":l="ltr"==m.direction?"right":"left";break;case "start":l="rtl"==m.direction?"right": +"left";break;default:l="left"}switch(this.textBaseline){case "hanging":case "top":t=h.size/1.75;break;case "middle":break;default:case null:case "alphabetic":case "ideographic":case "bottom":t=-h.size/2.25}switch(l){case "right":d=1E3;r=0.05;break;case "center":d=r=500}b=s(this,b+0,c+t);n.push('');e?S(this,n):T(this,n,{x:-d,y:0}, +{x:r,y:h.size});e=f[0][0].toFixed(3)+","+f[1][0].toFixed(3)+","+f[0][1].toFixed(3)+","+f[1][1].toFixed(3)+",0,0";b=k(b.x/q)+","+k(b.y/q);n.push('','','');this.element_.insertAdjacentHTML("beforeEnd",n.join(""))};d.fillText=function(a,b,c,d){this.drawText_(a,b,c,d,!1)};d.strokeText=function(a, +b,c,d){this.drawText_(a,b,c,d,!0)};d.measureText=function(a){this.textMeasureEl_||(this.element_.insertAdjacentHTML("beforeEnd",''),this.textMeasureEl_=this.element_.lastChild);var b=this.element_.ownerDocument;this.textMeasureEl_.innerHTML="";this.textMeasureEl_.style.font=this.font;this.textMeasureEl_.appendChild(b.createTextNode(a));return{width:this.textMeasureEl_.offsetWidth}};d.clip=function(){}; +d.arcTo=function(){};d.createPattern=function(a,b){return new I(a,b)};w.prototype.addColorStop=function(a,b){b=G(b);this.colors_.push({offset:a,color:b.color,alpha:b.alpha})};d=A.prototype=Error();d.INDEX_SIZE_ERR=1;d.DOMSTRING_SIZE_ERR=2;d.HIERARCHY_REQUEST_ERR=3;d.WRONG_DOCUMENT_ERR=4;d.INVALID_CHARACTER_ERR=5;d.NO_DATA_ALLOWED_ERR=6;d.NO_MODIFICATION_ALLOWED_ERR=7;d.NOT_FOUND_ERR=8;d.NOT_SUPPORTED_ERR=9;d.INUSE_ATTRIBUTE_ERR=10;d.INVALID_STATE_ERR=11;d.SYNTAX_ERR=12;d.INVALID_MODIFICATION_ERR= +13;d.NAMESPACE_ERR=14;d.INVALID_ACCESS_ERR=15;d.VALIDATION_ERR=16;d.TYPE_MISMATCH_ERR=17;G_vmlCanvasManager=U;CanvasRenderingContext2D=C;CanvasGradient=w;CanvasPattern=I;DOMException=A}(); +/*eslint-enable*/ +/*jshint ignore:end*/ \ No newline at end of file diff --git a/data/getSweep.json b/data/getSweep.json new file mode 100644 index 0000000..48c2113 --- /dev/null +++ b/data/getSweep.json @@ -0,0 +1,10 @@ +{ +"dispPoints": "290", +"start": "0.00", +"stop": "100000.00", +"attenuation": "0", +"levelOffset": "0", +"Points": [ +{"x":0.00,"y":-32},{"x":0.34,"y":-67},{"x":0.69,"y":-84},{"x":1.03,"y":-93},{"x":1.38,"y":-91},{"x":1.72,"y":-94},{"x":2.07,"y":-90},{"x":2.41,"y":-93},{"x":2.76,"y":-94},{"x":3.10,"y":-93},{"x":3.45,"y":-92},{"x":3.79,"y":-95},{"x":4.14,"y":-93},{"x":4.48,"y":-94},{"x":4.83,"y":-94},{"x":5.17,"y":-92},{"x":5.52,"y":-94},{"x":5.86,"y":-95},{"x":6.21,"y":-93},{"x":6.55,"y":-93},{"x":6.90,"y":-93},{"x":7.24,"y":-95},{"x":7.59,"y":-94},{"x":7.93,"y":-91},{"x":8.28,"y":-93},{"x":8.62,"y":-94},{"x":8.97,"y":-94},{"x":9.31,"y":-93},{"x":9.66,"y":-94},{"x":10.00,"y":-94},{"x":10.34,"y":-95},{"x":10.69,"y":-91},{"x":11.03,"y":-92},{"x":11.38,"y":-94},{"x":11.72,"y":-94},{"x":12.07,"y":-95},{"x":12.41,"y":-92},{"x":12.76,"y":-94},{"x":13.10,"y":-95},{"x":13.45,"y":-95},{"x":13.79,"y":-92},{"x":14.14,"y":-94},{"x":14.48,"y":-93},{"x":14.83,"y":-94},{"x":15.17,"y":-95},{"x":15.52,"y":-92},{"x":15.86,"y":-94},{"x":16.21,"y":-94},{"x":16.55,"y":-94},{"x":16.90,"y":-96},{"x":17.24,"y":-94},{"x":17.59,"y":-93},{"x":17.93,"y":-94},{"x":18.28,"y":-93},{"x":18.62,"y":-94},{"x":18.97,"y":-93},{"x":19.31,"y":-93},{"x":19.66,"y":-94},{"x":20.00,"y":-94},{"x":20.34,"y":-94},{"x":20.69,"y":-94},{"x":21.03,"y":-94},{"x":21.38,"y":-93},{"x":21.72,"y":-93},{"x":22.07,"y":-94},{"x":22.41,"y":-95},{"x":22.76,"y":-93},{"x":23.10,"y":-84},{"x":23.45,"y":-94},{"x":23.79,"y":-94},{"x":24.14,"y":-91},{"x":24.48,"y":-93},{"x":24.83,"y":-93},{"x":25.17,"y":-93},{"x":25.52,"y":-94},{"x":25.86,"y":-94},{"x":26.21,"y":-94},{"x":26.55,"y":-94},{"x":26.90,"y":-94},{"x":27.24,"y":-96},{"x":27.59,"y":-92},{"x":27.93,"y":-94},{"x":28.28,"y":-95},{"x":28.62,"y":-92},{"x":28.97,"y":-93},{"x":29.31,"y":-92},{"x":29.66,"y":-92},{"x":30.00,"y":-56},{"x":30.34,"y":-88},{"x":30.69,"y":-92},{"x":31.03,"y":-94},{"x":31.38,"y":-93},{"x":31.72,"y":-92},{"x":32.07,"y":-93},{"x":32.41,"y":-93},{"x":32.76,"y":-93},{"x":33.10,"y":-92},{"x":33.45,"y":-96},{"x":33.79,"y":-94},{"x":34.14,"y":-91},{"x":34.48,"y":-94},{"x":34.83,"y":-93},{"x":35.17,"y":-93},{"x":35.52,"y":-94},{"x":35.86,"y":-92},{"x":36.21,"y":-94},{"x":36.55,"y":-93},{"x":36.90,"y":-94},{"x":37.24,"y":-95},{"x":37.59,"y":-94},{"x":37.93,"y":-90},{"x":38.28,"y":-93},{"x":38.62,"y":-93},{"x":38.97,"y":-94},{"x":39.31,"y":-94},{"x":39.66,"y":-93},{"x":40.00,"y":-94},{"x":40.34,"y":-93},{"x":40.69,"y":-91},{"x":41.03,"y":-95},{"x":41.38,"y":-94},{"x":41.72,"y":-93},{"x":42.07,"y":-94},{"x":42.41,"y":-94},{"x":42.76,"y":-95},{"x":43.10,"y":-93},{"x":43.45,"y":-92},{"x":43.79,"y":-93},{"x":44.14,"y":-93},{"x":44.48,"y":-93},{"x":44.83,"y":-92},{"x":45.17,"y":-94},{"x":45.52,"y":-93},{"x":45.86,"y":-94},{"x":46.21,"y":-93},{"x":46.55,"y":-94},{"x":46.90,"y":-94},{"x":47.24,"y":-95},{"x":47.59,"y":-94},{"x":47.93,"y":-93},{"x":48.28,"y":-92},{"x":48.62,"y":-94},{"x":48.97,"y":-93},{"x":49.31,"y":-94},{"x":49.66,"y":-94},{"x":50.00,"y":-93},{"x":50.34,"y":-93},{"x":50.69,"y":-93},{"x":51.03,"y":-92},{"x":51.38,"y":-95},{"x":51.72,"y":-93},{"x":52.07,"y":-94},{"x":52.41,"y":-93},{"x":52.76,"y":-94},{"x":53.10,"y":-84},{"x":53.45,"y":-94},{"x":53.79,"y":-94},{"x":54.14,"y":-92},{"x":54.48,"y":-92},{"x":54.83,"y":-94},{"x":55.17,"y":-92},{"x":55.52,"y":-96},{"x":55.86,"y":-93},{"x":56.21,"y":-93},{"x":56.55,"y":-95},{"x":56.90,"y":-93},{"x":57.24,"y":-94},{"x":57.59,"y":-92},{"x":57.93,"y":-94},{"x":58.28,"y":-92},{"x":58.62,"y":-93},{"x":58.97,"y":-92},{"x":59.31,"y":-93},{"x":59.66,"y":-93},{"x":60.00,"y":-64},{"x":60.34,"y":-92},{"x":60.69,"y":-90},{"x":61.03,"y":-94},{"x":61.38,"y":-93},{"x":61.72,"y":-92},{"x":62.07,"y":-88},{"x":62.41,"y":-94},{"x":62.76,"y":-92},{"x":63.10,"y":-93},{"x":63.45,"y":-94},{"x":63.79,"y":-94},{"x":64.14,"y":-93},{"x":64.48,"y":-93},{"x":64.83,"y":-93},{"x":65.17,"y":-94},{"x":65.52,"y":-93},{"x":65.86,"y":-92},{"x":66.21,"y":-94},{"x":66.55,"y":-94},{"x":66.90,"y":-93},{"x":67.24,"y":-92},{"x":67.59,"y":-95},{"x":67.93,"y":-92},{"x":68.28,"y":-93},{"x":68.62,"y":-96},{"x":68.97,"y":-94},{"x":69.31,"y":-93},{"x":69.66,"y":-93},{"x":70.00,"y":-92},{"x":70.34,"y":-93},{"x":70.69,"y":-92},{"x":71.03,"y":-94},{"x":71.38,"y":-93},{"x":71.72,"y":-92},{"x":72.07,"y":-93},{"x":72.41,"y":-91},{"x":72.76,"y":-95},{"x":73.10,"y":-93},{"x":73.45,"y":-93},{"x":73.79,"y":-91},{"x":74.14,"y":-96},{"x":74.48,"y":-92},{"x":74.83,"y":-92},{"x":75.17,"y":-94},{"x":75.52,"y":-95},{"x":75.86,"y":-93},{"x":76.21,"y":-93},{"x":76.55,"y":-94},{"x":76.90,"y":-95},{"x":77.24,"y":-93},{"x":77.59,"y":-93},{"x":77.93,"y":-92},{"x":78.28,"y":-91},{"x":78.62,"y":-95},{"x":78.97,"y":-95},{"x":79.31,"y":-92},{"x":79.66,"y":-92},{"x":80.00,"y":-96},{"x":80.34,"y":-95},{"x":80.69,"y":-92},{"x":81.03,"y":-94},{"x":81.38,"y":-93},{"x":81.72,"y":-95},{"x":82.07,"y":-93},{"x":82.41,"y":-91},{"x":82.76,"y":-94},{"x":83.10,"y":-82},{"x":83.45,"y":-94},{"x":83.79,"y":-94},{"x":84.14,"y":-92},{"x":84.48,"y":-94},{"x":84.83,"y":-93},{"x":85.17,"y":-94},{"x":85.52,"y":-93},{"x":85.86,"y":-93},{"x":86.21,"y":-92},{"x":86.55,"y":-94},{"x":86.90,"y":-95},{"x":87.24,"y":-94},{"x":87.59,"y":-93},{"x":87.93,"y":-94},{"x":88.28,"y":-93},{"x":88.62,"y":-91},{"x":88.97,"y":-94},{"x":89.31,"y":-94},{"x":89.66,"y":-94},{"x":90.00,"y":-73},{"x":90.34,"y":-92},{"x":90.69,"y":-91},{"x":91.03,"y":-91},{"x":91.38,"y":-93},{"x":91.72,"y":-92},{"x":92.07,"y":-92},{"x":92.41,"y":-94},{"x":92.76,"y":-94},{"x":93.10,"y":-95},{"x":93.45,"y":-93},{"x":93.79,"y":-93},{"x":94.14,"y":-93},{"x":94.48,"y":-95},{"x":94.83,"y":-92},{"x":95.17,"y":-94},{"x":95.52,"y":-94},{"x":95.86,"y":-92},{"x":96.21,"y":-95},{"x":96.55,"y":-92},{"x":96.90,"y":-95},{"x":97.24,"y":-93},{"x":97.59,"y":-93},{"x":97.93,"y":-92},{"x":98.28,"y":-94},{"x":98.62,"y":-93},{"x":98.97,"y":-95},{"x":99.31,"y":-93},{"x":100.00,"y":-93} +] +} diff --git a/data/help.html b/data/help.html new file mode 100644 index 0000000..b180e13 --- /dev/null +++ b/data/help.html @@ -0,0 +1,115 @@ + + + + [RBNSpyBox] + + + + + + + + + + + +
+ +

+ [programName] [programVersion] +

+ + + + + + + +
+
+ + + +
+ +
+ +

Home

+ +

+ The Home page displays the current status of the RBNSpyBox and the status of all your "Friends". + This page will automatically refresh to display the latest "Friend" activity. +

+ +

Friends

+ +

+ The Friends page is used to maintain your list of "Friends", you can add upto a maximum of 50. +

+ +

+ RBNSpyBox comes pre-loaded with a number of example "Friends" and you will see these listed + on the Friends page. +

+ +

+ To update a "Friend" simply change the values in the boxes and hit the associated "Update" button. +

+ +

+ To remove a "Friend" simply hit the "Remove" button. +

+ +

+ At the bottom of the page you will find an area titled "Add New Friend". +

+ +

+ Here you can specify a callsign, name, comment and sound to be used when that particular "Friend" + is spotted. Callsigns can contain wildcards. Typically, taking the UK callsign G0PJO as an + example you will want to spot a "Friend" while working abroad, while within one of + the UK regions such as Wales, and possibly when out operating portable. To do this you can + specify the callsign as *G*0PJO* which will catch all occurrences. All fields are limited + to 20 characters. The associated sound can be disabled by unchecking the check-box. +

+ +

Settings

+ +

+ The Settings page is used to configure the RBNSpyBox. The main settings relate to Wi-Fi connectivity + but also includes settings for the Reverse Beacon Network interface and some miscellaneous settings. +

+ +

+ To commit all changes hit the "Update" button. +

+ +

Backup/Restore

+ +

+ The Backup/Restore page is used to backup and restore your list of "Friends". +

+ +

+ The "Friends" are backup they are persisted as a simple XML file. This file should be + securely saved away. To restore the friends simply drag and drop the file to the box + comtaining the text "Here...". +

+ +
+
+
+ + + diff --git a/data/index.html b/data/index.html new file mode 100644 index 0000000..2be6077 --- /dev/null +++ b/data/index.html @@ -0,0 +1,1788 @@ + + + + + + +simpleSA + + + + + + + + +
+ + + + +
+
+
+ +
+ + + +
+ +
+ +
+ + +
+
+ + + MHz +
+
+ + + MHz +
+
+
+ + + MHz +
+
+ + + + MHz +
+ +
+ +
+
+ + + kHz +
+
+
+ + + kHz +
+ +
+
+ + + dB + + + + dB +
+
+
+ + + MHz +
+
+
+
+ + +
+
+
+
+ + +
+ + + dBm +
+
+ + + MHz +
+ +
+
+
+
+ + pts + + ms +
+
+
+
+ + + + + +
+
+ + + +
+

Settings

+
+ + + dB + + + dB +
+
+ + + dBm +
+
+
+ + + dBm +
+
+ + + dB +
+
+ + + MHz +
+
+ + +
+ +
+ +
+ + + + + +
+
+
+
+
+
Host: + + + +
+
Status: + +
+
Pings: + +
+
Refresh: + + ms +
+
+
+ + +
+
+ + + + diff --git a/data/index2.html b/data/index2.html new file mode 100644 index 0000000..9ce791e --- /dev/null +++ b/data/index2.html @@ -0,0 +1,1264 @@ + + + + + + +TinySA-ESP + + + + + + + + +
+ + + + +
+
+
+
+ +
+ +

Sweep

+
+ + + MHz +
+
+ + + MHz +
+
+
+ + + MHz +
+
+ + + + MHz +
+ +
+ +
+
+ + + kHz +
+
+
+ + + kHz +
+ +
+
+ + + + dB +
+
+
+ + + MHz +
+
+
+
+ + +
+
+
+
+ + + + dBm +
+
+
+
+
+ + + + + ms +
+
+
+
+ + + + + +
+
+ + + + +
+

Settings

+
+ + + dB +
+
+ + + dBm +
+
+
+ + + dBm +
+
+ + + dB +
+
+ + + MHz +
+
+ + +
+ +
+ +
+ + + + + +
+
+
+
+
+
Host: + + + +
+
Status: + +
+
Pings: + +
+
Refresh Interval: + +
+
+
+ + +
+
+ + + + diff --git a/data/jquery-3.2.1.min.js b/data/jquery-3.2.1.min.js new file mode 100644 index 0000000..e0c9b5f --- /dev/null +++ b/data/jquery-3.2.1.min.js @@ -0,0 +1,12 @@ +/*! jQuery v3.2.1 | (c) JS Foundation and other contributors | jquery.org/license */ +!function (a, b) { "use strict"; "object" == typeof module && "object" == typeof module.exports ? module.exports = a.document ? b(a, !0) : function (a) { if (!a.document) throw new Error("jQuery requires a window with a document"); return b(a) } : b(a) }("undefined" != typeof window ? window : this, function (a, b) { + "use strict"; var c = [], d = a.document, e = Object.getPrototypeOf, f = c.slice, g = c.concat, h = c.push, i = c.indexOf, j = {}, k = j.toString, l = j.hasOwnProperty, m = l.toString, n = m.call(Object), o = {}; function p(a, b) { b = b || d; var c = b.createElement("script"); c.text = a, b.head.appendChild(c).parentNode.removeChild(c) } var q = "3.2.1", r = function (a, b) { return new r.fn.init(a, b) }, s = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, t = /^-ms-/, u = /-([a-z])/g, v = function (a, b) { return b.toUpperCase() }; r.fn = r.prototype = { jquery: q, constructor: r, length: 0, toArray: function () { return f.call(this) }, get: function (a) { return null == a ? f.call(this) : a < 0 ? this[a + this.length] : this[a] }, pushStack: function (a) { var b = r.merge(this.constructor(), a); return b.prevObject = this, b }, each: function (a) { return r.each(this, a) }, map: function (a) { return this.pushStack(r.map(this, function (b, c) { return a.call(b, c, b) })) }, slice: function () { return this.pushStack(f.apply(this, arguments)) }, first: function () { return this.eq(0) }, last: function () { return this.eq(-1) }, eq: function (a) { var b = this.length, c = +a + (a < 0 ? b : 0); return this.pushStack(c >= 0 && c < b ? [this[c]] : []) }, end: function () { return this.prevObject || this.constructor() }, push: h, sort: c.sort, splice: c.splice }, r.extend = r.fn.extend = function () { var a, b, c, d, e, f, g = arguments[0] || {}, h = 1, i = arguments.length, j = !1; for ("boolean" == typeof g && (j = g, g = arguments[h] || {}, h++), "object" == typeof g || r.isFunction(g) || (g = {}), h === i && (g = this, h--); h < i; h++) if (null != (a = arguments[h])) for (b in a) c = g[b], d = a[b], g !== d && (j && d && (r.isPlainObject(d) || (e = Array.isArray(d))) ? (e ? (e = !1, f = c && Array.isArray(c) ? c : []) : f = c && r.isPlainObject(c) ? c : {}, g[b] = r.extend(j, f, d)) : void 0 !== d && (g[b] = d)); return g }, r.extend({ expando: "jQuery" + (q + Math.random()).replace(/\D/g, ""), isReady: !0, error: function (a) { throw new Error(a) }, noop: function () { }, isFunction: function (a) { return "function" === r.type(a) }, isWindow: function (a) { return null != a && a === a.window }, isNumeric: function (a) { var b = r.type(a); return ("number" === b || "string" === b) && !isNaN(a - parseFloat(a)) }, isPlainObject: function (a) { var b, c; return !(!a || "[object Object]" !== k.call(a)) && (!(b = e(a)) || (c = l.call(b, "constructor") && b.constructor, "function" == typeof c && m.call(c) === n)) }, isEmptyObject: function (a) { var b; for (b in a) return !1; return !0 }, type: function (a) { return null == a ? a + "" : "object" == typeof a || "function" == typeof a ? j[k.call(a)] || "object" : typeof a }, globalEval: function (a) { p(a) }, camelCase: function (a) { return a.replace(t, "ms-").replace(u, v) }, each: function (a, b) { var c, d = 0; if (w(a)) { for (c = a.length; d < c; d++) if (b.call(a[d], d, a[d]) === !1) break } else for (d in a) if (b.call(a[d], d, a[d]) === !1) break; return a }, trim: function (a) { return null == a ? "" : (a + "").replace(s, "") }, makeArray: function (a, b) { var c = b || []; return null != a && (w(Object(a)) ? r.merge(c, "string" == typeof a ? [a] : a) : h.call(c, a)), c }, inArray: function (a, b, c) { return null == b ? -1 : i.call(b, a, c) }, merge: function (a, b) { for (var c = +b.length, d = 0, e = a.length; d < c; d++) a[e++] = b[d]; return a.length = e, a }, grep: function (a, b, c) { for (var d, e = [], f = 0, g = a.length, h = !c; f < g; f++) d = !b(a[f], f), d !== h && e.push(a[f]); return e }, map: function (a, b, c) { var d, e, f = 0, h = []; if (w(a)) for (d = a.length; f < d; f++) e = b(a[f], f, c), null != e && h.push(e); else for (f in a) e = b(a[f], f, c), null != e && h.push(e); return g.apply([], h) }, guid: 1, proxy: function (a, b) { var c, d, e; if ("string" == typeof b && (c = a[b], b = a, a = c), r.isFunction(a)) return d = f.call(arguments, 2), e = function () { return a.apply(b || this, d.concat(f.call(arguments))) }, e.guid = a.guid = a.guid || r.guid++ , e }, now: Date.now, support: o }), "function" == typeof Symbol && (r.fn[Symbol.iterator] = c[Symbol.iterator]), r.each("Boolean Number String Function Array Date RegExp Object Error Symbol".split(" "), function (a, b) { j["[object " + b + "]"] = b.toLowerCase() }); function w(a) { var b = !!a && "length" in a && a.length, c = r.type(a); return "function" !== c && !r.isWindow(a) && ("array" === c || 0 === b || "number" == typeof b && b > 0 && b - 1 in a) } var x = function (a) { var b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u = "sizzle" + 1 * new Date, v = a.document, w = 0, x = 0, y = ha(), z = ha(), A = ha(), B = function (a, b) { return a === b && (l = !0), 0 }, C = {}.hasOwnProperty, D = [], E = D.pop, F = D.push, G = D.push, H = D.slice, I = function (a, b) { for (var c = 0, d = a.length; c < d; c++) if (a[c] === b) return c; return -1 }, J = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped", K = "[\\x20\\t\\r\\n\\f]", L = "(?:\\\\.|[\\w-]|[^\0-\\xa0])+", M = "\\[" + K + "*(" + L + ")(?:" + K + "*([*^$|!~]?=)" + K + "*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|(" + L + "))|)" + K + "*\\]", N = ":(" + L + ")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|" + M + ")*)|.*)\\)|)", O = new RegExp(K + "+", "g"), P = new RegExp("^" + K + "+|((?:^|[^\\\\])(?:\\\\.)*)" + K + "+$", "g"), Q = new RegExp("^" + K + "*," + K + "*"), R = new RegExp("^" + K + "*([>+~]|" + K + ")" + K + "*"), S = new RegExp("=" + K + "*([^\\]'\"]*?)" + K + "*\\]", "g"), T = new RegExp(N), U = new RegExp("^" + L + "$"), V = { ID: new RegExp("^#(" + L + ")"), CLASS: new RegExp("^\\.(" + L + ")"), TAG: new RegExp("^(" + L + "|[*])"), ATTR: new RegExp("^" + M), PSEUDO: new RegExp("^" + N), CHILD: new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + K + "*(even|odd|(([+-]|)(\\d*)n|)" + K + "*(?:([+-]|)" + K + "*(\\d+)|))" + K + "*\\)|)", "i"), bool: new RegExp("^(?:" + J + ")$", "i"), needsContext: new RegExp("^" + K + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + K + "*((?:-\\d)?\\d*)" + K + "*\\)|)(?=[^-]|$)", "i") }, W = /^(?:input|select|textarea|button)$/i, X = /^h\d$/i, Y = /^[^{]+\{\s*\[native \w/, Z = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/, $ = /[+~]/, _ = new RegExp("\\\\([\\da-f]{1,6}" + K + "?|(" + K + ")|.)", "ig"), aa = function (a, b, c) { var d = "0x" + b - 65536; return d !== d || c ? b : d < 0 ? String.fromCharCode(d + 65536) : String.fromCharCode(d >> 10 | 55296, 1023 & d | 56320) }, ba = /([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g, ca = function (a, b) { return b ? "\0" === a ? "\ufffd" : a.slice(0, -1) + "\\" + a.charCodeAt(a.length - 1).toString(16) + " " : "\\" + a }, da = function () { m() }, ea = ta(function (a) { return a.disabled === !0 && ("form" in a || "label" in a) }, { dir: "parentNode", next: "legend" }); try { G.apply(D = H.call(v.childNodes), v.childNodes), D[v.childNodes.length].nodeType } catch (fa) { G = { apply: D.length ? function (a, b) { F.apply(a, H.call(b)) } : function (a, b) { var c = a.length, d = 0; while (a[c++] = b[d++]); a.length = c - 1 } } } function ga(a, b, d, e) { var f, h, j, k, l, o, r, s = b && b.ownerDocument, w = b ? b.nodeType : 9; if (d = d || [], "string" != typeof a || !a || 1 !== w && 9 !== w && 11 !== w) return d; if (!e && ((b ? b.ownerDocument || b : v) !== n && m(b), b = b || n, p)) { if (11 !== w && (l = Z.exec(a))) if (f = l[1]) { if (9 === w) { if (!(j = b.getElementById(f))) return d; if (j.id === f) return d.push(j), d } else if (s && (j = s.getElementById(f)) && t(b, j) && j.id === f) return d.push(j), d } else { if (l[2]) return G.apply(d, b.getElementsByTagName(a)), d; if ((f = l[3]) && c.getElementsByClassName && b.getElementsByClassName) return G.apply(d, b.getElementsByClassName(f)), d } if (c.qsa && !A[a + " "] && (!q || !q.test(a))) { if (1 !== w) s = b, r = a; else if ("object" !== b.nodeName.toLowerCase()) { (k = b.getAttribute("id")) ? k = k.replace(ba, ca) : b.setAttribute("id", k = u), o = g(a), h = o.length; while (h--) o[h] = "#" + k + " " + sa(o[h]); r = o.join(","), s = $.test(a) && qa(b.parentNode) || b } if (r) try { return G.apply(d, s.querySelectorAll(r)), d } catch (x) { } finally { k === u && b.removeAttribute("id") } } } return i(a.replace(P, "$1"), b, d, e) } function ha() { var a = []; function b(c, e) { return a.push(c + " ") > d.cacheLength && delete b[a.shift()], b[c + " "] = e } return b } function ia(a) { return a[u] = !0, a } function ja(a) { var b = n.createElement("fieldset"); try { return !!a(b) } catch (c) { return !1 } finally { b.parentNode && b.parentNode.removeChild(b), b = null } } function ka(a, b) { var c = a.split("|"), e = c.length; while (e--) d.attrHandle[c[e]] = b } function la(a, b) { var c = b && a, d = c && 1 === a.nodeType && 1 === b.nodeType && a.sourceIndex - b.sourceIndex; if (d) return d; if (c) while (c = c.nextSibling) if (c === b) return -1; return a ? 1 : -1 } function ma(a) { return function (b) { var c = b.nodeName.toLowerCase(); return "input" === c && b.type === a } } function na(a) { return function (b) { var c = b.nodeName.toLowerCase(); return ("input" === c || "button" === c) && b.type === a } } function oa(a) { return function (b) { return "form" in b ? b.parentNode && b.disabled === !1 ? "label" in b ? "label" in b.parentNode ? b.parentNode.disabled === a : b.disabled === a : b.isDisabled === a || b.isDisabled !== !a && ea(b) === a : b.disabled === a : "label" in b && b.disabled === a } } function pa(a) { return ia(function (b) { return b = +b, ia(function (c, d) { var e, f = a([], c.length, b), g = f.length; while (g--) c[e = f[g]] && (c[e] = !(d[e] = c[e])) }) }) } function qa(a) { return a && "undefined" != typeof a.getElementsByTagName && a } c = ga.support = {}, f = ga.isXML = function (a) { var b = a && (a.ownerDocument || a).documentElement; return !!b && "HTML" !== b.nodeName }, m = ga.setDocument = function (a) { var b, e, g = a ? a.ownerDocument || a : v; return g !== n && 9 === g.nodeType && g.documentElement ? (n = g, o = n.documentElement, p = !f(n), v !== n && (e = n.defaultView) && e.top !== e && (e.addEventListener ? e.addEventListener("unload", da, !1) : e.attachEvent && e.attachEvent("onunload", da)), c.attributes = ja(function (a) { return a.className = "i", !a.getAttribute("className") }), c.getElementsByTagName = ja(function (a) { return a.appendChild(n.createComment("")), !a.getElementsByTagName("*").length }), c.getElementsByClassName = Y.test(n.getElementsByClassName), c.getById = ja(function (a) { return o.appendChild(a).id = u, !n.getElementsByName || !n.getElementsByName(u).length }), c.getById ? (d.filter.ID = function (a) { var b = a.replace(_, aa); return function (a) { return a.getAttribute("id") === b } }, d.find.ID = function (a, b) { if ("undefined" != typeof b.getElementById && p) { var c = b.getElementById(a); return c ? [c] : [] } }) : (d.filter.ID = function (a) { var b = a.replace(_, aa); return function (a) { var c = "undefined" != typeof a.getAttributeNode && a.getAttributeNode("id"); return c && c.value === b } }, d.find.ID = function (a, b) { if ("undefined" != typeof b.getElementById && p) { var c, d, e, f = b.getElementById(a); if (f) { if (c = f.getAttributeNode("id"), c && c.value === a) return [f]; e = b.getElementsByName(a), d = 0; while (f = e[d++]) if (c = f.getAttributeNode("id"), c && c.value === a) return [f] } return [] } }), d.find.TAG = c.getElementsByTagName ? function (a, b) { return "undefined" != typeof b.getElementsByTagName ? b.getElementsByTagName(a) : c.qsa ? b.querySelectorAll(a) : void 0 } : function (a, b) { var c, d = [], e = 0, f = b.getElementsByTagName(a); if ("*" === a) { while (c = f[e++]) 1 === c.nodeType && d.push(c); return d } return f }, d.find.CLASS = c.getElementsByClassName && function (a, b) { if ("undefined" != typeof b.getElementsByClassName && p) return b.getElementsByClassName(a) }, r = [], q = [], (c.qsa = Y.test(n.querySelectorAll)) && (ja(function (a) { o.appendChild(a).innerHTML = "", a.querySelectorAll("[msallowcapture^='']").length && q.push("[*^$]=" + K + "*(?:''|\"\")"), a.querySelectorAll("[selected]").length || q.push("\\[" + K + "*(?:value|" + J + ")"), a.querySelectorAll("[id~=" + u + "-]").length || q.push("~="), a.querySelectorAll(":checked").length || q.push(":checked"), a.querySelectorAll("a#" + u + "+*").length || q.push(".#.+[+~]") }), ja(function (a) { a.innerHTML = ""; var b = n.createElement("input"); b.setAttribute("type", "hidden"), a.appendChild(b).setAttribute("name", "D"), a.querySelectorAll("[name=d]").length && q.push("name" + K + "*[*^$|!~]?="), 2 !== a.querySelectorAll(":enabled").length && q.push(":enabled", ":disabled"), o.appendChild(a).disabled = !0, 2 !== a.querySelectorAll(":disabled").length && q.push(":enabled", ":disabled"), a.querySelectorAll("*,:x"), q.push(",.*:") })), (c.matchesSelector = Y.test(s = o.matches || o.webkitMatchesSelector || o.mozMatchesSelector || o.oMatchesSelector || o.msMatchesSelector)) && ja(function (a) { c.disconnectedMatch = s.call(a, "*"), s.call(a, "[s!='']:x"), r.push("!=", N) }), q = q.length && new RegExp(q.join("|")), r = r.length && new RegExp(r.join("|")), b = Y.test(o.compareDocumentPosition), t = b || Y.test(o.contains) ? function (a, b) { var c = 9 === a.nodeType ? a.documentElement : a, d = b && b.parentNode; return a === d || !(!d || 1 !== d.nodeType || !(c.contains ? c.contains(d) : a.compareDocumentPosition && 16 & a.compareDocumentPosition(d))) } : function (a, b) { if (b) while (b = b.parentNode) if (b === a) return !0; return !1 }, B = b ? function (a, b) { if (a === b) return l = !0, 0; var d = !a.compareDocumentPosition - !b.compareDocumentPosition; return d ? d : (d = (a.ownerDocument || a) === (b.ownerDocument || b) ? a.compareDocumentPosition(b) : 1, 1 & d || !c.sortDetached && b.compareDocumentPosition(a) === d ? a === n || a.ownerDocument === v && t(v, a) ? -1 : b === n || b.ownerDocument === v && t(v, b) ? 1 : k ? I(k, a) - I(k, b) : 0 : 4 & d ? -1 : 1) } : function (a, b) { if (a === b) return l = !0, 0; var c, d = 0, e = a.parentNode, f = b.parentNode, g = [a], h = [b]; if (!e || !f) return a === n ? -1 : b === n ? 1 : e ? -1 : f ? 1 : k ? I(k, a) - I(k, b) : 0; if (e === f) return la(a, b); c = a; while (c = c.parentNode) g.unshift(c); c = b; while (c = c.parentNode) h.unshift(c); while (g[d] === h[d]) d++; return d ? la(g[d], h[d]) : g[d] === v ? -1 : h[d] === v ? 1 : 0 }, n) : n }, ga.matches = function (a, b) { return ga(a, null, null, b) }, ga.matchesSelector = function (a, b) { if ((a.ownerDocument || a) !== n && m(a), b = b.replace(S, "='$1']"), c.matchesSelector && p && !A[b + " "] && (!r || !r.test(b)) && (!q || !q.test(b))) try { var d = s.call(a, b); if (d || c.disconnectedMatch || a.document && 11 !== a.document.nodeType) return d } catch (e) { } return ga(b, n, null, [a]).length > 0 }, ga.contains = function (a, b) { return (a.ownerDocument || a) !== n && m(a), t(a, b) }, ga.attr = function (a, b) { (a.ownerDocument || a) !== n && m(a); var e = d.attrHandle[b.toLowerCase()], f = e && C.call(d.attrHandle, b.toLowerCase()) ? e(a, b, !p) : void 0; return void 0 !== f ? f : c.attributes || !p ? a.getAttribute(b) : (f = a.getAttributeNode(b)) && f.specified ? f.value : null }, ga.escape = function (a) { return (a + "").replace(ba, ca) }, ga.error = function (a) { throw new Error("Syntax error, unrecognized expression: " + a) }, ga.uniqueSort = function (a) { var b, d = [], e = 0, f = 0; if (l = !c.detectDuplicates, k = !c.sortStable && a.slice(0), a.sort(B), l) { while (b = a[f++]) b === a[f] && (e = d.push(f)); while (e--) a.splice(d[e], 1) } return k = null, a }, e = ga.getText = function (a) { var b, c = "", d = 0, f = a.nodeType; if (f) { if (1 === f || 9 === f || 11 === f) { if ("string" == typeof a.textContent) return a.textContent; for (a = a.firstChild; a; a = a.nextSibling) c += e(a) } else if (3 === f || 4 === f) return a.nodeValue } else while (b = a[d++]) c += e(b); return c }, d = ga.selectors = { cacheLength: 50, createPseudo: ia, match: V, attrHandle: {}, find: {}, relative: { ">": { dir: "parentNode", first: !0 }, " ": { dir: "parentNode" }, "+": { dir: "previousSibling", first: !0 }, "~": { dir: "previousSibling" } }, preFilter: { ATTR: function (a) { return a[1] = a[1].replace(_, aa), a[3] = (a[3] || a[4] || a[5] || "").replace(_, aa), "~=" === a[2] && (a[3] = " " + a[3] + " "), a.slice(0, 4) }, CHILD: function (a) { return a[1] = a[1].toLowerCase(), "nth" === a[1].slice(0, 3) ? (a[3] || ga.error(a[0]), a[4] = +(a[4] ? a[5] + (a[6] || 1) : 2 * ("even" === a[3] || "odd" === a[3])), a[5] = +(a[7] + a[8] || "odd" === a[3])) : a[3] && ga.error(a[0]), a }, PSEUDO: function (a) { var b, c = !a[6] && a[2]; return V.CHILD.test(a[0]) ? null : (a[3] ? a[2] = a[4] || a[5] || "" : c && T.test(c) && (b = g(c, !0)) && (b = c.indexOf(")", c.length - b) - c.length) && (a[0] = a[0].slice(0, b), a[2] = c.slice(0, b)), a.slice(0, 3)) } }, filter: { TAG: function (a) { var b = a.replace(_, aa).toLowerCase(); return "*" === a ? function () { return !0 } : function (a) { return a.nodeName && a.nodeName.toLowerCase() === b } }, CLASS: function (a) { var b = y[a + " "]; return b || (b = new RegExp("(^|" + K + ")" + a + "(" + K + "|$)")) && y(a, function (a) { return b.test("string" == typeof a.className && a.className || "undefined" != typeof a.getAttribute && a.getAttribute("class") || "") }) }, ATTR: function (a, b, c) { return function (d) { var e = ga.attr(d, a); return null == e ? "!=" === b : !b || (e += "", "=" === b ? e === c : "!=" === b ? e !== c : "^=" === b ? c && 0 === e.indexOf(c) : "*=" === b ? c && e.indexOf(c) > -1 : "$=" === b ? c && e.slice(-c.length) === c : "~=" === b ? (" " + e.replace(O, " ") + " ").indexOf(c) > -1 : "|=" === b && (e === c || e.slice(0, c.length + 1) === c + "-")) } }, CHILD: function (a, b, c, d, e) { var f = "nth" !== a.slice(0, 3), g = "last" !== a.slice(-4), h = "of-type" === b; return 1 === d && 0 === e ? function (a) { return !!a.parentNode } : function (b, c, i) { var j, k, l, m, n, o, p = f !== g ? "nextSibling" : "previousSibling", q = b.parentNode, r = h && b.nodeName.toLowerCase(), s = !i && !h, t = !1; if (q) { if (f) { while (p) { m = b; while (m = m[p]) if (h ? m.nodeName.toLowerCase() === r : 1 === m.nodeType) return !1; o = p = "only" === a && !o && "nextSibling" } return !0 } if (o = [g ? q.firstChild : q.lastChild], g && s) { m = q, l = m[u] || (m[u] = {}), k = l[m.uniqueID] || (l[m.uniqueID] = {}), j = k[a] || [], n = j[0] === w && j[1], t = n && j[2], m = n && q.childNodes[n]; while (m = ++n && m && m[p] || (t = n = 0) || o.pop()) if (1 === m.nodeType && ++t && m === b) { k[a] = [w, n, t]; break } } else if (s && (m = b, l = m[u] || (m[u] = {}), k = l[m.uniqueID] || (l[m.uniqueID] = {}), j = k[a] || [], n = j[0] === w && j[1], t = n), t === !1) while (m = ++n && m && m[p] || (t = n = 0) || o.pop()) if ((h ? m.nodeName.toLowerCase() === r : 1 === m.nodeType) && ++t && (s && (l = m[u] || (m[u] = {}), k = l[m.uniqueID] || (l[m.uniqueID] = {}), k[a] = [w, t]), m === b)) break; return t -= e, t === d || t % d === 0 && t / d >= 0 } } }, PSEUDO: function (a, b) { var c, e = d.pseudos[a] || d.setFilters[a.toLowerCase()] || ga.error("unsupported pseudo: " + a); return e[u] ? e(b) : e.length > 1 ? (c = [a, a, "", b], d.setFilters.hasOwnProperty(a.toLowerCase()) ? ia(function (a, c) { var d, f = e(a, b), g = f.length; while (g--) d = I(a, f[g]), a[d] = !(c[d] = f[g]) }) : function (a) { return e(a, 0, c) }) : e } }, pseudos: { not: ia(function (a) { var b = [], c = [], d = h(a.replace(P, "$1")); return d[u] ? ia(function (a, b, c, e) { var f, g = d(a, null, e, []), h = a.length; while (h--) (f = g[h]) && (a[h] = !(b[h] = f)) }) : function (a, e, f) { return b[0] = a, d(b, null, f, c), b[0] = null, !c.pop() } }), has: ia(function (a) { return function (b) { return ga(a, b).length > 0 } }), contains: ia(function (a) { return a = a.replace(_, aa), function (b) { return (b.textContent || b.innerText || e(b)).indexOf(a) > -1 } }), lang: ia(function (a) { return U.test(a || "") || ga.error("unsupported lang: " + a), a = a.replace(_, aa).toLowerCase(), function (b) { var c; do if (c = p ? b.lang : b.getAttribute("xml:lang") || b.getAttribute("lang")) return c = c.toLowerCase(), c === a || 0 === c.indexOf(a + "-"); while ((b = b.parentNode) && 1 === b.nodeType); return !1 } }), target: function (b) { var c = a.location && a.location.hash; return c && c.slice(1) === b.id }, root: function (a) { return a === o }, focus: function (a) { return a === n.activeElement && (!n.hasFocus || n.hasFocus()) && !!(a.type || a.href || ~a.tabIndex) }, enabled: oa(!1), disabled: oa(!0), checked: function (a) { var b = a.nodeName.toLowerCase(); return "input" === b && !!a.checked || "option" === b && !!a.selected }, selected: function (a) { return a.parentNode && a.parentNode.selectedIndex, a.selected === !0 }, empty: function (a) { for (a = a.firstChild; a; a = a.nextSibling) if (a.nodeType < 6) return !1; return !0 }, parent: function (a) { return !d.pseudos.empty(a) }, header: function (a) { return X.test(a.nodeName) }, input: function (a) { return W.test(a.nodeName) }, button: function (a) { var b = a.nodeName.toLowerCase(); return "input" === b && "button" === a.type || "button" === b }, text: function (a) { var b; return "input" === a.nodeName.toLowerCase() && "text" === a.type && (null == (b = a.getAttribute("type")) || "text" === b.toLowerCase()) }, first: pa(function () { return [0] }), last: pa(function (a, b) { return [b - 1] }), eq: pa(function (a, b, c) { return [c < 0 ? c + b : c] }), even: pa(function (a, b) { for (var c = 0; c < b; c += 2) a.push(c); return a }), odd: pa(function (a, b) { for (var c = 1; c < b; c += 2) a.push(c); return a }), lt: pa(function (a, b, c) { for (var d = c < 0 ? c + b : c; --d >= 0;) a.push(d); return a }), gt: pa(function (a, b, c) { for (var d = c < 0 ? c + b : c; ++d < b;) a.push(d); return a }) } }, d.pseudos.nth = d.pseudos.eq; for (b in { radio: !0, checkbox: !0, file: !0, password: !0, image: !0 }) d.pseudos[b] = ma(b); for (b in { submit: !0, reset: !0 }) d.pseudos[b] = na(b); function ra() { } ra.prototype = d.filters = d.pseudos, d.setFilters = new ra, g = ga.tokenize = function (a, b) { var c, e, f, g, h, i, j, k = z[a + " "]; if (k) return b ? 0 : k.slice(0); h = a, i = [], j = d.preFilter; while (h) { c && !(e = Q.exec(h)) || (e && (h = h.slice(e[0].length) || h), i.push(f = [])), c = !1, (e = R.exec(h)) && (c = e.shift(), f.push({ value: c, type: e[0].replace(P, " ") }), h = h.slice(c.length)); for (g in d.filter) !(e = V[g].exec(h)) || j[g] && !(e = j[g](e)) || (c = e.shift(), f.push({ value: c, type: g, matches: e }), h = h.slice(c.length)); if (!c) break } return b ? h.length : h ? ga.error(a) : z(a, i).slice(0) }; function sa(a) { for (var b = 0, c = a.length, d = ""; b < c; b++) d += a[b].value; return d } function ta(a, b, c) { var d = b.dir, e = b.next, f = e || d, g = c && "parentNode" === f, h = x++; return b.first ? function (b, c, e) { while (b = b[d]) if (1 === b.nodeType || g) return a(b, c, e); return !1 } : function (b, c, i) { var j, k, l, m = [w, h]; if (i) { while (b = b[d]) if ((1 === b.nodeType || g) && a(b, c, i)) return !0 } else while (b = b[d]) if (1 === b.nodeType || g) if (l = b[u] || (b[u] = {}), k = l[b.uniqueID] || (l[b.uniqueID] = {}), e && e === b.nodeName.toLowerCase()) b = b[d] || b; else { if ((j = k[f]) && j[0] === w && j[1] === h) return m[2] = j[2]; if (k[f] = m, m[2] = a(b, c, i)) return !0 } return !1 } } function ua(a) { return a.length > 1 ? function (b, c, d) { var e = a.length; while (e--) if (!a[e](b, c, d)) return !1; return !0 } : a[0] } function va(a, b, c) { for (var d = 0, e = b.length; d < e; d++) ga(a, b[d], c); return c } function wa(a, b, c, d, e) { for (var f, g = [], h = 0, i = a.length, j = null != b; h < i; h++) (f = a[h]) && (c && !c(f, d, e) || (g.push(f), j && b.push(h))); return g } function xa(a, b, c, d, e, f) { return d && !d[u] && (d = xa(d)), e && !e[u] && (e = xa(e, f)), ia(function (f, g, h, i) { var j, k, l, m = [], n = [], o = g.length, p = f || va(b || "*", h.nodeType ? [h] : h, []), q = !a || !f && b ? p : wa(p, m, a, h, i), r = c ? e || (f ? a : o || d) ? [] : g : q; if (c && c(q, r, h, i), d) { j = wa(r, n), d(j, [], h, i), k = j.length; while (k--) (l = j[k]) && (r[n[k]] = !(q[n[k]] = l)) } if (f) { if (e || a) { if (e) { j = [], k = r.length; while (k--) (l = r[k]) && j.push(q[k] = l); e(null, r = [], j, i) } k = r.length; while (k--) (l = r[k]) && (j = e ? I(f, l) : m[k]) > -1 && (f[j] = !(g[j] = l)) } } else r = wa(r === g ? r.splice(o, r.length) : r), e ? e(null, g, r, i) : G.apply(g, r) }) } function ya(a) { for (var b, c, e, f = a.length, g = d.relative[a[0].type], h = g || d.relative[" "], i = g ? 1 : 0, k = ta(function (a) { return a === b }, h, !0), l = ta(function (a) { return I(b, a) > -1 }, h, !0), m = [function (a, c, d) { var e = !g && (d || c !== j) || ((b = c).nodeType ? k(a, c, d) : l(a, c, d)); return b = null, e }]; i < f; i++) if (c = d.relative[a[i].type]) m = [ta(ua(m), c)]; else { if (c = d.filter[a[i].type].apply(null, a[i].matches), c[u]) { for (e = ++i; e < f; e++) if (d.relative[a[e].type]) break; return xa(i > 1 && ua(m), i > 1 && sa(a.slice(0, i - 1).concat({ value: " " === a[i - 2].type ? "*" : "" })).replace(P, "$1"), c, i < e && ya(a.slice(i, e)), e < f && ya(a = a.slice(e)), e < f && sa(a)) } m.push(c) } return ua(m) } function za(a, b) { var c = b.length > 0, e = a.length > 0, f = function (f, g, h, i, k) { var l, o, q, r = 0, s = "0", t = f && [], u = [], v = j, x = f || e && d.find.TAG("*", k), y = w += null == v ? 1 : Math.random() || .1, z = x.length; for (k && (j = g === n || g || k); s !== z && null != (l = x[s]); s++) { if (e && l) { o = 0, g || l.ownerDocument === n || (m(l), h = !p); while (q = a[o++]) if (q(l, g || n, h)) { i.push(l); break } k && (w = y) } c && ((l = !q && l) && r-- , f && t.push(l)) } if (r += s, c && s !== r) { o = 0; while (q = b[o++]) q(t, u, g, h); if (f) { if (r > 0) while (s--) t[s] || u[s] || (u[s] = E.call(i)); u = wa(u) } G.apply(i, u), k && !f && u.length > 0 && r + b.length > 1 && ga.uniqueSort(i) } return k && (w = y, j = v), t }; return c ? ia(f) : f } return h = ga.compile = function (a, b) { var c, d = [], e = [], f = A[a + " "]; if (!f) { b || (b = g(a)), c = b.length; while (c--) f = ya(b[c]), f[u] ? d.push(f) : e.push(f); f = A(a, za(e, d)), f.selector = a } return f }, i = ga.select = function (a, b, c, e) { var f, i, j, k, l, m = "function" == typeof a && a, n = !e && g(a = m.selector || a); if (c = c || [], 1 === n.length) { if (i = n[0] = n[0].slice(0), i.length > 2 && "ID" === (j = i[0]).type && 9 === b.nodeType && p && d.relative[i[1].type]) { if (b = (d.find.ID(j.matches[0].replace(_, aa), b) || [])[0], !b) return c; m && (b = b.parentNode), a = a.slice(i.shift().value.length) } f = V.needsContext.test(a) ? 0 : i.length; while (f--) { if (j = i[f], d.relative[k = j.type]) break; if ((l = d.find[k]) && (e = l(j.matches[0].replace(_, aa), $.test(i[0].type) && qa(b.parentNode) || b))) { if (i.splice(f, 1), a = e.length && sa(i), !a) return G.apply(c, e), c; break } } } return (m || h(a, n))(e, b, !p, c, !b || $.test(a) && qa(b.parentNode) || b), c }, c.sortStable = u.split("").sort(B).join("") === u, c.detectDuplicates = !!l, m(), c.sortDetached = ja(function (a) { return 1 & a.compareDocumentPosition(n.createElement("fieldset")) }), ja(function (a) { return a.innerHTML = "", "#" === a.firstChild.getAttribute("href") }) || ka("type|href|height|width", function (a, b, c) { if (!c) return a.getAttribute(b, "type" === b.toLowerCase() ? 1 : 2) }), c.attributes && ja(function (a) { return a.innerHTML = "", a.firstChild.setAttribute("value", ""), "" === a.firstChild.getAttribute("value") }) || ka("value", function (a, b, c) { if (!c && "input" === a.nodeName.toLowerCase()) return a.defaultValue }), ja(function (a) { return null == a.getAttribute("disabled") }) || ka(J, function (a, b, c) { var d; if (!c) return a[b] === !0 ? b.toLowerCase() : (d = a.getAttributeNode(b)) && d.specified ? d.value : null }), ga }(a); r.find = x, r.expr = x.selectors, r.expr[":"] = r.expr.pseudos, r.uniqueSort = r.unique = x.uniqueSort, r.text = x.getText, r.isXMLDoc = x.isXML, r.contains = x.contains, r.escapeSelector = x.escape; var y = function (a, b, c) { var d = [], e = void 0 !== c; while ((a = a[b]) && 9 !== a.nodeType) if (1 === a.nodeType) { if (e && r(a).is(c)) break; d.push(a) } return d }, z = function (a, b) { for (var c = []; a; a = a.nextSibling) 1 === a.nodeType && a !== b && c.push(a); return c }, A = r.expr.match.needsContext; function B(a, b) { return a.nodeName && a.nodeName.toLowerCase() === b.toLowerCase() } var C = /^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i, D = /^.[^:#\[\.,]*$/; function E(a, b, c) { return r.isFunction(b) ? r.grep(a, function (a, d) { return !!b.call(a, d, a) !== c }) : b.nodeType ? r.grep(a, function (a) { return a === b !== c }) : "string" != typeof b ? r.grep(a, function (a) { return i.call(b, a) > -1 !== c }) : D.test(b) ? r.filter(b, a, c) : (b = r.filter(b, a), r.grep(a, function (a) { return i.call(b, a) > -1 !== c && 1 === a.nodeType })) } r.filter = function (a, b, c) { var d = b[0]; return c && (a = ":not(" + a + ")"), 1 === b.length && 1 === d.nodeType ? r.find.matchesSelector(d, a) ? [d] : [] : r.find.matches(a, r.grep(b, function (a) { return 1 === a.nodeType })) }, r.fn.extend({ find: function (a) { var b, c, d = this.length, e = this; if ("string" != typeof a) return this.pushStack(r(a).filter(function () { for (b = 0; b < d; b++) if (r.contains(e[b], this)) return !0 })); for (c = this.pushStack([]), b = 0; b < d; b++) r.find(a, e[b], c); return d > 1 ? r.uniqueSort(c) : c }, filter: function (a) { return this.pushStack(E(this, a || [], !1)) }, not: function (a) { return this.pushStack(E(this, a || [], !0)) }, is: function (a) { return !!E(this, "string" == typeof a && A.test(a) ? r(a) : a || [], !1).length } }); var F, G = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/, H = r.fn.init = function (a, b, c) { var e, f; if (!a) return this; if (c = c || F, "string" == typeof a) { if (e = "<" === a[0] && ">" === a[a.length - 1] && a.length >= 3 ? [null, a, null] : G.exec(a), !e || !e[1] && b) return !b || b.jquery ? (b || c).find(a) : this.constructor(b).find(a); if (e[1]) { if (b = b instanceof r ? b[0] : b, r.merge(this, r.parseHTML(e[1], b && b.nodeType ? b.ownerDocument || b : d, !0)), C.test(e[1]) && r.isPlainObject(b)) for (e in b) r.isFunction(this[e]) ? this[e](b[e]) : this.attr(e, b[e]); return this } return f = d.getElementById(e[2]), f && (this[0] = f, this.length = 1), this } return a.nodeType ? (this[0] = a, this.length = 1, this) : r.isFunction(a) ? void 0 !== c.ready ? c.ready(a) : a(r) : r.makeArray(a, this) }; H.prototype = r.fn, F = r(d); var I = /^(?:parents|prev(?:Until|All))/, J = { children: !0, contents: !0, next: !0, prev: !0 }; r.fn.extend({ has: function (a) { var b = r(a, this), c = b.length; return this.filter(function () { for (var a = 0; a < c; a++) if (r.contains(this, b[a])) return !0 }) }, closest: function (a, b) { var c, d = 0, e = this.length, f = [], g = "string" != typeof a && r(a); if (!A.test(a)) for (; d < e; d++) for (c = this[d]; c && c !== b; c = c.parentNode) if (c.nodeType < 11 && (g ? g.index(c) > -1 : 1 === c.nodeType && r.find.matchesSelector(c, a))) { f.push(c); break } return this.pushStack(f.length > 1 ? r.uniqueSort(f) : f) }, index: function (a) { return a ? "string" == typeof a ? i.call(r(a), this[0]) : i.call(this, a.jquery ? a[0] : a) : this[0] && this[0].parentNode ? this.first().prevAll().length : -1 }, add: function (a, b) { return this.pushStack(r.uniqueSort(r.merge(this.get(), r(a, b)))) }, addBack: function (a) { return this.add(null == a ? this.prevObject : this.prevObject.filter(a)) } }); function K(a, b) { while ((a = a[b]) && 1 !== a.nodeType); return a } r.each({ parent: function (a) { var b = a.parentNode; return b && 11 !== b.nodeType ? b : null }, parents: function (a) { return y(a, "parentNode") }, parentsUntil: function (a, b, c) { return y(a, "parentNode", c) }, next: function (a) { return K(a, "nextSibling") }, prev: function (a) { return K(a, "previousSibling") }, nextAll: function (a) { return y(a, "nextSibling") }, prevAll: function (a) { return y(a, "previousSibling") }, nextUntil: function (a, b, c) { return y(a, "nextSibling", c) }, prevUntil: function (a, b, c) { return y(a, "previousSibling", c) }, siblings: function (a) { return z((a.parentNode || {}).firstChild, a) }, children: function (a) { return z(a.firstChild) }, contents: function (a) { return B(a, "iframe") ? a.contentDocument : (B(a, "template") && (a = a.content || a), r.merge([], a.childNodes)) } }, function (a, b) { r.fn[a] = function (c, d) { var e = r.map(this, b, c); return "Until" !== a.slice(-5) && (d = c), d && "string" == typeof d && (e = r.filter(d, e)), this.length > 1 && (J[a] || r.uniqueSort(e), I.test(a) && e.reverse()), this.pushStack(e) } }); var L = /[^\x20\t\r\n\f]+/g; function M(a) { var b = {}; return r.each(a.match(L) || [], function (a, c) { b[c] = !0 }), b } r.Callbacks = function (a) { a = "string" == typeof a ? M(a) : r.extend({}, a); var b, c, d, e, f = [], g = [], h = -1, i = function () { for (e = e || a.once, d = b = !0; g.length; h = -1) { c = g.shift(); while (++h < f.length) f[h].apply(c[0], c[1]) === !1 && a.stopOnFalse && (h = f.length, c = !1) } a.memory || (c = !1), b = !1, e && (f = c ? [] : "") }, j = { add: function () { return f && (c && !b && (h = f.length - 1, g.push(c)), function d(b) { r.each(b, function (b, c) { r.isFunction(c) ? a.unique && j.has(c) || f.push(c) : c && c.length && "string" !== r.type(c) && d(c) }) }(arguments), c && !b && i()), this }, remove: function () { return r.each(arguments, function (a, b) { var c; while ((c = r.inArray(b, f, c)) > -1) f.splice(c, 1), c <= h && h-- }), this }, has: function (a) { return a ? r.inArray(a, f) > -1 : f.length > 0 }, empty: function () { return f && (f = []), this }, disable: function () { return e = g = [], f = c = "", this }, disabled: function () { return !f }, lock: function () { return e = g = [], c || b || (f = c = ""), this }, locked: function () { return !!e }, fireWith: function (a, c) { return e || (c = c || [], c = [a, c.slice ? c.slice() : c], g.push(c), b || i()), this }, fire: function () { return j.fireWith(this, arguments), this }, fired: function () { return !!d } }; return j }; function N(a) { return a } function O(a) { throw a } function P(a, b, c, d) { var e; try { a && r.isFunction(e = a.promise) ? e.call(a).done(b).fail(c) : a && r.isFunction(e = a.then) ? e.call(a, b, c) : b.apply(void 0, [a].slice(d)) } catch (a) { c.apply(void 0, [a]) } } r.extend({ Deferred: function (b) { var c = [["notify", "progress", r.Callbacks("memory"), r.Callbacks("memory"), 2], ["resolve", "done", r.Callbacks("once memory"), r.Callbacks("once memory"), 0, "resolved"], ["reject", "fail", r.Callbacks("once memory"), r.Callbacks("once memory"), 1, "rejected"]], d = "pending", e = { state: function () { return d }, always: function () { return f.done(arguments).fail(arguments), this }, "catch": function (a) { return e.then(null, a) }, pipe: function () { var a = arguments; return r.Deferred(function (b) { r.each(c, function (c, d) { var e = r.isFunction(a[d[4]]) && a[d[4]]; f[d[1]](function () { var a = e && e.apply(this, arguments); a && r.isFunction(a.promise) ? a.promise().progress(b.notify).done(b.resolve).fail(b.reject) : b[d[0] + "With"](this, e ? [a] : arguments) }) }), a = null }).promise() }, then: function (b, d, e) { var f = 0; function g(b, c, d, e) { return function () { var h = this, i = arguments, j = function () { var a, j; if (!(b < f)) { if (a = d.apply(h, i), a === c.promise()) throw new TypeError("Thenable self-resolution"); j = a && ("object" == typeof a || "function" == typeof a) && a.then, r.isFunction(j) ? e ? j.call(a, g(f, c, N, e), g(f, c, O, e)) : (f++ , j.call(a, g(f, c, N, e), g(f, c, O, e), g(f, c, N, c.notifyWith))) : (d !== N && (h = void 0, i = [a]), (e || c.resolveWith)(h, i)) } }, k = e ? j : function () { try { j() } catch (a) { r.Deferred.exceptionHook && r.Deferred.exceptionHook(a, k.stackTrace), b + 1 >= f && (d !== O && (h = void 0, i = [a]), c.rejectWith(h, i)) } }; b ? k() : (r.Deferred.getStackHook && (k.stackTrace = r.Deferred.getStackHook()), a.setTimeout(k)) } } return r.Deferred(function (a) { c[0][3].add(g(0, a, r.isFunction(e) ? e : N, a.notifyWith)), c[1][3].add(g(0, a, r.isFunction(b) ? b : N)), c[2][3].add(g(0, a, r.isFunction(d) ? d : O)) }).promise() }, promise: function (a) { return null != a ? r.extend(a, e) : e } }, f = {}; return r.each(c, function (a, b) { var g = b[2], h = b[5]; e[b[1]] = g.add, h && g.add(function () { d = h }, c[3 - a][2].disable, c[0][2].lock), g.add(b[3].fire), f[b[0]] = function () { return f[b[0] + "With"](this === f ? void 0 : this, arguments), this }, f[b[0] + "With"] = g.fireWith }), e.promise(f), b && b.call(f, f), f }, when: function (a) { var b = arguments.length, c = b, d = Array(c), e = f.call(arguments), g = r.Deferred(), h = function (a) { return function (c) { d[a] = this, e[a] = arguments.length > 1 ? f.call(arguments) : c, --b || g.resolveWith(d, e) } }; if (b <= 1 && (P(a, g.done(h(c)).resolve, g.reject, !b), "pending" === g.state() || r.isFunction(e[c] && e[c].then))) return g.then(); while (c--) P(e[c], h(c), g.reject); return g.promise() } }); var Q = /^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/; r.Deferred.exceptionHook = function (b, c) { a.console && a.console.warn && b && Q.test(b.name) && a.console.warn("jQuery.Deferred exception: " + b.message, b.stack, c) }, r.readyException = function (b) { a.setTimeout(function () { throw b }) }; var R = r.Deferred(); r.fn.ready = function (a) { return R.then(a)["catch"](function (a) { r.readyException(a) }), this }, r.extend({ isReady: !1, readyWait: 1, ready: function (a) { (a === !0 ? --r.readyWait : r.isReady) || (r.isReady = !0, a !== !0 && --r.readyWait > 0 || R.resolveWith(d, [r])) } }), r.ready.then = R.then; function S() { + d.removeEventListener("DOMContentLoaded", S), + a.removeEventListener("load", S), r.ready() + } "complete" === d.readyState || "loading" !== d.readyState && !d.documentElement.doScroll ? a.setTimeout(r.ready) : (d.addEventListener("DOMContentLoaded", S), a.addEventListener("load", S)); var T = function (a, b, c, d, e, f, g) { var h = 0, i = a.length, j = null == c; if ("object" === r.type(c)) { e = !0; for (h in c) T(a, b, h, c[h], !0, f, g) } else if (void 0 !== d && (e = !0, r.isFunction(d) || (g = !0), j && (g ? (b.call(a, d), b = null) : (j = b, b = function (a, b, c) { return j.call(r(a), c) })), b)) for (; h < i; h++) b(a[h], c, g ? d : d.call(a[h], h, b(a[h], c))); return e ? a : j ? b.call(a) : i ? b(a[0], c) : f }, U = function (a) { return 1 === a.nodeType || 9 === a.nodeType || !+a.nodeType }; function V() { this.expando = r.expando + V.uid++ } V.uid = 1, V.prototype = { cache: function (a) { var b = a[this.expando]; return b || (b = {}, U(a) && (a.nodeType ? a[this.expando] = b : Object.defineProperty(a, this.expando, { value: b, configurable: !0 }))), b }, set: function (a, b, c) { var d, e = this.cache(a); if ("string" == typeof b) e[r.camelCase(b)] = c; else for (d in b) e[r.camelCase(d)] = b[d]; return e }, get: function (a, b) { return void 0 === b ? this.cache(a) : a[this.expando] && a[this.expando][r.camelCase(b)] }, access: function (a, b, c) { return void 0 === b || b && "string" == typeof b && void 0 === c ? this.get(a, b) : (this.set(a, b, c), void 0 !== c ? c : b) }, remove: function (a, b) { var c, d = a[this.expando]; if (void 0 !== d) { if (void 0 !== b) { Array.isArray(b) ? b = b.map(r.camelCase) : (b = r.camelCase(b), b = b in d ? [b] : b.match(L) || []), c = b.length; while (c--) delete d[b[c]] } (void 0 === b || r.isEmptyObject(d)) && (a.nodeType ? a[this.expando] = void 0 : delete a[this.expando]) } }, hasData: function (a) { var b = a[this.expando]; return void 0 !== b && !r.isEmptyObject(b) } }; var W = new V, X = new V, Y = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/, Z = /[A-Z]/g; function $(a) { return "true" === a || "false" !== a && ("null" === a ? null : a === +a + "" ? +a : Y.test(a) ? JSON.parse(a) : a) } function _(a, b, c) { var d; if (void 0 === c && 1 === a.nodeType) if (d = "data-" + b.replace(Z, "-$&").toLowerCase(), c = a.getAttribute(d), "string" == typeof c) { try { c = $(c) } catch (e) { } X.set(a, b, c) } else c = void 0; return c } r.extend({ hasData: function (a) { return X.hasData(a) || W.hasData(a) }, data: function (a, b, c) { return X.access(a, b, c) }, removeData: function (a, b) { X.remove(a, b) }, _data: function (a, b, c) { return W.access(a, b, c) }, _removeData: function (a, b) { W.remove(a, b) } }), r.fn.extend({ data: function (a, b) { var c, d, e, f = this[0], g = f && f.attributes; if (void 0 === a) { if (this.length && (e = X.get(f), 1 === f.nodeType && !W.get(f, "hasDataAttrs"))) { c = g.length; while (c--) g[c] && (d = g[c].name, 0 === d.indexOf("data-") && (d = r.camelCase(d.slice(5)), _(f, d, e[d]))); W.set(f, "hasDataAttrs", !0) } return e } return "object" == typeof a ? this.each(function () { X.set(this, a) }) : T(this, function (b) { var c; if (f && void 0 === b) { if (c = X.get(f, a), void 0 !== c) return c; if (c = _(f, a), void 0 !== c) return c } else this.each(function () { X.set(this, a, b) }) }, null, b, arguments.length > 1, null, !0) }, removeData: function (a) { return this.each(function () { X.remove(this, a) }) } }), r.extend({ queue: function (a, b, c) { var d; if (a) return b = (b || "fx") + "queue", d = W.get(a, b), c && (!d || Array.isArray(c) ? d = W.access(a, b, r.makeArray(c)) : d.push(c)), d || [] }, dequeue: function (a, b) { b = b || "fx"; var c = r.queue(a, b), d = c.length, e = c.shift(), f = r._queueHooks(a, b), g = function () { r.dequeue(a, b) }; "inprogress" === e && (e = c.shift(), d--), e && ("fx" === b && c.unshift("inprogress"), delete f.stop, e.call(a, g, f)), !d && f && f.empty.fire() }, _queueHooks: function (a, b) { var c = b + "queueHooks"; return W.get(a, c) || W.access(a, c, { empty: r.Callbacks("once memory").add(function () { W.remove(a, [b + "queue", c]) }) }) } }), r.fn.extend({ queue: function (a, b) { var c = 2; return "string" != typeof a && (b = a, a = "fx", c--), arguments.length < c ? r.queue(this[0], a) : void 0 === b ? this : this.each(function () { var c = r.queue(this, a, b); r._queueHooks(this, a), "fx" === a && "inprogress" !== c[0] && r.dequeue(this, a) }) }, dequeue: function (a) { return this.each(function () { r.dequeue(this, a) }) }, clearQueue: function (a) { return this.queue(a || "fx", []) }, promise: function (a, b) { var c, d = 1, e = r.Deferred(), f = this, g = this.length, h = function () { --d || e.resolveWith(f, [f]) }; "string" != typeof a && (b = a, a = void 0), a = a || "fx"; while (g--) c = W.get(f[g], a + "queueHooks"), c && c.empty && (d++ , c.empty.add(h)); return h(), e.promise(b) } }); var aa = /[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source, ba = new RegExp("^(?:([+-])=|)(" + aa + ")([a-z%]*)$", "i"), ca = ["Top", "Right", "Bottom", "Left"], da = function (a, b) { return a = b || a, "none" === a.style.display || "" === a.style.display && r.contains(a.ownerDocument, a) && "none" === r.css(a, "display") }, ea = function (a, b, c, d) { var e, f, g = {}; for (f in b) g[f] = a.style[f], a.style[f] = b[f]; e = c.apply(a, d || []); for (f in b) a.style[f] = g[f]; return e }; function fa(a, b, c, d) { var e, f = 1, g = 20, h = d ? function () { return d.cur() } : function () { return r.css(a, b, "") }, i = h(), j = c && c[3] || (r.cssNumber[b] ? "" : "px"), k = (r.cssNumber[b] || "px" !== j && +i) && ba.exec(r.css(a, b)); if (k && k[3] !== j) { j = j || k[3], c = c || [], k = +i || 1; do f = f || ".5", k /= f, r.style(a, b, k + j); while (f !== (f = h() / i) && 1 !== f && --g) } return c && (k = +k || +i || 0, e = c[1] ? k + (c[1] + 1) * c[2] : +c[2], d && (d.unit = j, d.start = k, d.end = e)), e } var ga = {}; function ha(a) { var b, c = a.ownerDocument, d = a.nodeName, e = ga[d]; return e ? e : (b = c.body.appendChild(c.createElement(d)), e = r.css(b, "display"), b.parentNode.removeChild(b), "none" === e && (e = "block"), ga[d] = e, e) } function ia(a, b) { for (var c, d, e = [], f = 0, g = a.length; f < g; f++) d = a[f], d.style && (c = d.style.display, b ? ("none" === c && (e[f] = W.get(d, "display") || null, e[f] || (d.style.display = "")), "" === d.style.display && da(d) && (e[f] = ha(d))) : "none" !== c && (e[f] = "none", W.set(d, "display", c))); for (f = 0; f < g; f++) null != e[f] && (a[f].style.display = e[f]); return a } r.fn.extend({ show: function () { return ia(this, !0) }, hide: function () { return ia(this) }, toggle: function (a) { return "boolean" == typeof a ? a ? this.show() : this.hide() : this.each(function () { da(this) ? r(this).show() : r(this).hide() }) } }); var ja = /^(?:checkbox|radio)$/i, ka = /<([a-z][^\/\0>\x20\t\r\n\f]+)/i, la = /^$|\/(?:java|ecma)script/i, ma = { option: [1, ""], thead: [1, "", "
"], col: [2, "", "
"], tr: [2, "", "
"], td: [3, "", "
"], _default: [0, "", ""] }; ma.optgroup = ma.option, ma.tbody = ma.tfoot = ma.colgroup = ma.caption = ma.thead, ma.th = ma.td; function na(a, b) { var c; return c = "undefined" != typeof a.getElementsByTagName ? a.getElementsByTagName(b || "*") : "undefined" != typeof a.querySelectorAll ? a.querySelectorAll(b || "*") : [], void 0 === b || b && B(a, b) ? r.merge([a], c) : c } function oa(a, b) { for (var c = 0, d = a.length; c < d; c++) W.set(a[c], "globalEval", !b || W.get(b[c], "globalEval")) } var pa = /<|&#?\w+;/; function qa(a, b, c, d, e) { for (var f, g, h, i, j, k, l = b.createDocumentFragment(), m = [], n = 0, o = a.length; n < o; n++) if (f = a[n], f || 0 === f) if ("object" === r.type(f)) r.merge(m, f.nodeType ? [f] : f); else if (pa.test(f)) { g = g || l.appendChild(b.createElement("div")), h = (ka.exec(f) || ["", ""])[1].toLowerCase(), i = ma[h] || ma._default, g.innerHTML = i[1] + r.htmlPrefilter(f) + i[2], k = i[0]; while (k--) g = g.lastChild; r.merge(m, g.childNodes), g = l.firstChild, g.textContent = "" } else m.push(b.createTextNode(f)); l.textContent = "", n = 0; while (f = m[n++]) if (d && r.inArray(f, d) > -1) e && e.push(f); else if (j = r.contains(f.ownerDocument, f), g = na(l.appendChild(f), "script"), j && oa(g), c) { k = 0; while (f = g[k++]) la.test(f.type || "") && c.push(f) } return l } !function () { var a = d.createDocumentFragment(), b = a.appendChild(d.createElement("div")), c = d.createElement("input"); c.setAttribute("type", "radio"), c.setAttribute("checked", "checked"), c.setAttribute("name", "t"), b.appendChild(c), o.checkClone = b.cloneNode(!0).cloneNode(!0).lastChild.checked, b.innerHTML = "", o.noCloneChecked = !!b.cloneNode(!0).lastChild.defaultValue }(); var ra = d.documentElement, sa = /^key/, ta = /^(?:mouse|pointer|contextmenu|drag|drop)|click/, ua = /^([^.]*)(?:\.(.+)|)/; function va() { return !0 } function wa() { return !1 } function xa() { try { return d.activeElement } catch (a) { } } function ya(a, b, c, d, e, f) { var g, h; if ("object" == typeof b) { "string" != typeof c && (d = d || c, c = void 0); for (h in b) ya(a, h, c, d, b[h], f); return a } if (null == d && null == e ? (e = c, d = c = void 0) : null == e && ("string" == typeof c ? (e = d, d = void 0) : (e = d, d = c, c = void 0)), e === !1) e = wa; else if (!e) return a; return 1 === f && (g = e, e = function (a) { return r().off(a), g.apply(this, arguments) }, e.guid = g.guid || (g.guid = r.guid++)), a.each(function () { r.event.add(this, b, e, d, c) }) } r.event = { global: {}, add: function (a, b, c, d, e) { var f, g, h, i, j, k, l, m, n, o, p, q = W.get(a); if (q) { c.handler && (f = c, c = f.handler, e = f.selector), e && r.find.matchesSelector(ra, e), c.guid || (c.guid = r.guid++), (i = q.events) || (i = q.events = {}), (g = q.handle) || (g = q.handle = function (b) { return "undefined" != typeof r && r.event.triggered !== b.type ? r.event.dispatch.apply(a, arguments) : void 0 }), b = (b || "").match(L) || [""], j = b.length; while (j--) h = ua.exec(b[j]) || [], n = p = h[1], o = (h[2] || "").split(".").sort(), n && (l = r.event.special[n] || {}, n = (e ? l.delegateType : l.bindType) || n, l = r.event.special[n] || {}, k = r.extend({ type: n, origType: p, data: d, handler: c, guid: c.guid, selector: e, needsContext: e && r.expr.match.needsContext.test(e), namespace: o.join(".") }, f), (m = i[n]) || (m = i[n] = [], m.delegateCount = 0, l.setup && l.setup.call(a, d, o, g) !== !1 || a.addEventListener && a.addEventListener(n, g)), l.add && (l.add.call(a, k), k.handler.guid || (k.handler.guid = c.guid)), e ? m.splice(m.delegateCount++, 0, k) : m.push(k), r.event.global[n] = !0) } }, remove: function (a, b, c, d, e) { var f, g, h, i, j, k, l, m, n, o, p, q = W.hasData(a) && W.get(a); if (q && (i = q.events)) { b = (b || "").match(L) || [""], j = b.length; while (j--) if (h = ua.exec(b[j]) || [], n = p = h[1], o = (h[2] || "").split(".").sort(), n) { l = r.event.special[n] || {}, n = (d ? l.delegateType : l.bindType) || n, m = i[n] || [], h = h[2] && new RegExp("(^|\\.)" + o.join("\\.(?:.*\\.|)") + "(\\.|$)"), g = f = m.length; while (f--) k = m[f], !e && p !== k.origType || c && c.guid !== k.guid || h && !h.test(k.namespace) || d && d !== k.selector && ("**" !== d || !k.selector) || (m.splice(f, 1), k.selector && m.delegateCount-- , l.remove && l.remove.call(a, k)); g && !m.length && (l.teardown && l.teardown.call(a, o, q.handle) !== !1 || r.removeEvent(a, n, q.handle), delete i[n]) } else for (n in i) r.event.remove(a, n + b[j], c, d, !0); r.isEmptyObject(i) && W.remove(a, "handle events") } }, dispatch: function (a) { var b = r.event.fix(a), c, d, e, f, g, h, i = new Array(arguments.length), j = (W.get(this, "events") || {})[b.type] || [], k = r.event.special[b.type] || {}; for (i[0] = b, c = 1; c < arguments.length; c++) i[c] = arguments[c]; if (b.delegateTarget = this, !k.preDispatch || k.preDispatch.call(this, b) !== !1) { h = r.event.handlers.call(this, b, j), c = 0; while ((f = h[c++]) && !b.isPropagationStopped()) { b.currentTarget = f.elem, d = 0; while ((g = f.handlers[d++]) && !b.isImmediatePropagationStopped()) b.rnamespace && !b.rnamespace.test(g.namespace) || (b.handleObj = g, b.data = g.data, e = ((r.event.special[g.origType] || {}).handle || g.handler).apply(f.elem, i), void 0 !== e && (b.result = e) === !1 && (b.preventDefault(), b.stopPropagation())) } return k.postDispatch && k.postDispatch.call(this, b), b.result } }, handlers: function (a, b) { var c, d, e, f, g, h = [], i = b.delegateCount, j = a.target; if (i && j.nodeType && !("click" === a.type && a.button >= 1)) for (; j !== this; j = j.parentNode || this) if (1 === j.nodeType && ("click" !== a.type || j.disabled !== !0)) { for (f = [], g = {}, c = 0; c < i; c++) d = b[c], e = d.selector + " ", void 0 === g[e] && (g[e] = d.needsContext ? r(e, this).index(j) > -1 : r.find(e, this, null, [j]).length), g[e] && f.push(d); f.length && h.push({ elem: j, handlers: f }) } return j = this, i < b.length && h.push({ elem: j, handlers: b.slice(i) }), h }, addProp: function (a, b) { Object.defineProperty(r.Event.prototype, a, { enumerable: !0, configurable: !0, get: r.isFunction(b) ? function () { if (this.originalEvent) return b(this.originalEvent) } : function () { if (this.originalEvent) return this.originalEvent[a] }, set: function (b) { Object.defineProperty(this, a, { enumerable: !0, configurable: !0, writable: !0, value: b }) } }) }, fix: function (a) { return a[r.expando] ? a : new r.Event(a) }, special: { load: { noBubble: !0 }, focus: { trigger: function () { if (this !== xa() && this.focus) return this.focus(), !1 }, delegateType: "focusin" }, blur: { trigger: function () { if (this === xa() && this.blur) return this.blur(), !1 }, delegateType: "focusout" }, click: { trigger: function () { if ("checkbox" === this.type && this.click && B(this, "input")) return this.click(), !1 }, _default: function (a) { return B(a.target, "a") } }, beforeunload: { postDispatch: function (a) { void 0 !== a.result && a.originalEvent && (a.originalEvent.returnValue = a.result) } } } }, r.removeEvent = function (a, b, c) { a.removeEventListener && a.removeEventListener(b, c) }, r.Event = function (a, b) { return this instanceof r.Event ? (a && a.type ? (this.originalEvent = a, this.type = a.type, this.isDefaultPrevented = a.defaultPrevented || void 0 === a.defaultPrevented && a.returnValue === !1 ? va : wa, this.target = a.target && 3 === a.target.nodeType ? a.target.parentNode : a.target, this.currentTarget = a.currentTarget, this.relatedTarget = a.relatedTarget) : this.type = a, b && r.extend(this, b), this.timeStamp = a && a.timeStamp || r.now(), void (this[r.expando] = !0)) : new r.Event(a, b) }, r.Event.prototype = { constructor: r.Event, isDefaultPrevented: wa, isPropagationStopped: wa, isImmediatePropagationStopped: wa, isSimulated: !1, preventDefault: function () { var a = this.originalEvent; this.isDefaultPrevented = va, a && !this.isSimulated && a.preventDefault() }, stopPropagation: function () { var a = this.originalEvent; this.isPropagationStopped = va, a && !this.isSimulated && a.stopPropagation() }, stopImmediatePropagation: function () { var a = this.originalEvent; this.isImmediatePropagationStopped = va, a && !this.isSimulated && a.stopImmediatePropagation(), this.stopPropagation() } }, r.each({ altKey: !0, bubbles: !0, cancelable: !0, changedTouches: !0, ctrlKey: !0, detail: !0, eventPhase: !0, metaKey: !0, pageX: !0, pageY: !0, shiftKey: !0, view: !0, "char": !0, charCode: !0, key: !0, keyCode: !0, button: !0, buttons: !0, clientX: !0, clientY: !0, offsetX: !0, offsetY: !0, pointerId: !0, pointerType: !0, screenX: !0, screenY: !0, targetTouches: !0, toElement: !0, touches: !0, which: function (a) { var b = a.button; return null == a.which && sa.test(a.type) ? null != a.charCode ? a.charCode : a.keyCode : !a.which && void 0 !== b && ta.test(a.type) ? 1 & b ? 1 : 2 & b ? 3 : 4 & b ? 2 : 0 : a.which } }, r.event.addProp), r.each({ mouseenter: "mouseover", mouseleave: "mouseout", pointerenter: "pointerover", pointerleave: "pointerout" }, function (a, b) { r.event.special[a] = { delegateType: b, bindType: b, handle: function (a) { var c, d = this, e = a.relatedTarget, f = a.handleObj; return e && (e === d || r.contains(d, e)) || (a.type = f.origType, c = f.handler.apply(this, arguments), a.type = b), c } } }), r.fn.extend({ on: function (a, b, c, d) { return ya(this, a, b, c, d) }, one: function (a, b, c, d) { return ya(this, a, b, c, d, 1) }, off: function (a, b, c) { var d, e; if (a && a.preventDefault && a.handleObj) return d = a.handleObj, r(a.delegateTarget).off(d.namespace ? d.origType + "." + d.namespace : d.origType, d.selector, d.handler), this; if ("object" == typeof a) { for (e in a) this.off(e, b, a[e]); return this } return b !== !1 && "function" != typeof b || (c = b, b = void 0), c === !1 && (c = wa), this.each(function () { r.event.remove(this, a, c, b) }) } }); var za = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([a-z][^\/\0>\x20\t\r\n\f]*)[^>]*)\/>/gi, Aa = /\s*$/g; function Ea(a, b) { return B(a, "table") && B(11 !== b.nodeType ? b : b.firstChild, "tr") ? r(">tbody", a)[0] || a : a } function Fa(a) { return a.type = (null !== a.getAttribute("type")) + "/" + a.type, a } function Ga(a) { var b = Ca.exec(a.type); return b ? a.type = b[1] : a.removeAttribute("type"), a } function Ha(a, b) { var c, d, e, f, g, h, i, j; if (1 === b.nodeType) { if (W.hasData(a) && (f = W.access(a), g = W.set(b, f), j = f.events)) { delete g.handle, g.events = {}; for (e in j) for (c = 0, d = j[e].length; c < d; c++) r.event.add(b, e, j[e][c]) } X.hasData(a) && (h = X.access(a), i = r.extend({}, h), X.set(b, i)) } } function Ia(a, b) { var c = b.nodeName.toLowerCase(); "input" === c && ja.test(a.type) ? b.checked = a.checked : "input" !== c && "textarea" !== c || (b.defaultValue = a.defaultValue) } function Ja(a, b, c, d) { b = g.apply([], b); var e, f, h, i, j, k, l = 0, m = a.length, n = m - 1, q = b[0], s = r.isFunction(q); if (s || m > 1 && "string" == typeof q && !o.checkClone && Ba.test(q)) return a.each(function (e) { var f = a.eq(e); s && (b[0] = q.call(this, e, f.html())), Ja(f, b, c, d) }); if (m && (e = qa(b, a[0].ownerDocument, !1, a, d), f = e.firstChild, 1 === e.childNodes.length && (e = f), f || d)) { for (h = r.map(na(e, "script"), Fa), i = h.length; l < m; l++) j = e, l !== n && (j = r.clone(j, !0, !0), i && r.merge(h, na(j, "script"))), c.call(a[l], j, l); if (i) for (k = h[h.length - 1].ownerDocument, r.map(h, Ga), l = 0; l < i; l++) j = h[l], la.test(j.type || "") && !W.access(j, "globalEval") && r.contains(k, j) && (j.src ? r._evalUrl && r._evalUrl(j.src) : p(j.textContent.replace(Da, ""), k)) } return a } function Ka(a, b, c) { for (var d, e = b ? r.filter(b, a) : a, f = 0; null != (d = e[f]); f++) c || 1 !== d.nodeType || r.cleanData(na(d)), d.parentNode && (c && r.contains(d.ownerDocument, d) && oa(na(d, "script")), d.parentNode.removeChild(d)); return a } r.extend({ htmlPrefilter: function (a) { return a.replace(za, "<$1>") }, clone: function (a, b, c) { var d, e, f, g, h = a.cloneNode(!0), i = r.contains(a.ownerDocument, a); if (!(o.noCloneChecked || 1 !== a.nodeType && 11 !== a.nodeType || r.isXMLDoc(a))) for (g = na(h), f = na(a), d = 0, e = f.length; d < e; d++) Ia(f[d], g[d]); if (b) if (c) for (f = f || na(a), g = g || na(h), d = 0, e = f.length; d < e; d++) Ha(f[d], g[d]); else Ha(a, h); return g = na(h, "script"), g.length > 0 && oa(g, !i && na(a, "script")), h }, cleanData: function (a) { for (var b, c, d, e = r.event.special, f = 0; void 0 !== (c = a[f]); f++) if (U(c)) { if (b = c[W.expando]) { if (b.events) for (d in b.events) e[d] ? r.event.remove(c, d) : r.removeEvent(c, d, b.handle); c[W.expando] = void 0 } c[X.expando] && (c[X.expando] = void 0) } } }), r.fn.extend({ detach: function (a) { return Ka(this, a, !0) }, remove: function (a) { return Ka(this, a) }, text: function (a) { return T(this, function (a) { return void 0 === a ? r.text(this) : this.empty().each(function () { 1 !== this.nodeType && 11 !== this.nodeType && 9 !== this.nodeType || (this.textContent = a) }) }, null, a, arguments.length) }, append: function () { return Ja(this, arguments, function (a) { if (1 === this.nodeType || 11 === this.nodeType || 9 === this.nodeType) { var b = Ea(this, a); b.appendChild(a) } }) }, prepend: function () { return Ja(this, arguments, function (a) { if (1 === this.nodeType || 11 === this.nodeType || 9 === this.nodeType) { var b = Ea(this, a); b.insertBefore(a, b.firstChild) } }) }, before: function () { return Ja(this, arguments, function (a) { this.parentNode && this.parentNode.insertBefore(a, this) }) }, after: function () { return Ja(this, arguments, function (a) { this.parentNode && this.parentNode.insertBefore(a, this.nextSibling) }) }, empty: function () { for (var a, b = 0; null != (a = this[b]); b++) 1 === a.nodeType && (r.cleanData(na(a, !1)), a.textContent = ""); return this }, clone: function (a, b) { return a = null != a && a, b = null == b ? a : b, this.map(function () { return r.clone(this, a, b) }) }, html: function (a) { return T(this, function (a) { var b = this[0] || {}, c = 0, d = this.length; if (void 0 === a && 1 === b.nodeType) return b.innerHTML; if ("string" == typeof a && !Aa.test(a) && !ma[(ka.exec(a) || ["", ""])[1].toLowerCase()]) { a = r.htmlPrefilter(a); try { for (; c < d; c++) b = this[c] || {}, 1 === b.nodeType && (r.cleanData(na(b, !1)), b.innerHTML = a); b = 0 } catch (e) { } } b && this.empty().append(a) }, null, a, arguments.length) }, replaceWith: function () { var a = []; return Ja(this, arguments, function (b) { var c = this.parentNode; r.inArray(this, a) < 0 && (r.cleanData(na(this)), c && c.replaceChild(b, this)) }, a) } }), r.each({ appendTo: "append", prependTo: "prepend", insertBefore: "before", insertAfter: "after", replaceAll: "replaceWith" }, function (a, b) { r.fn[a] = function (a) { for (var c, d = [], e = r(a), f = e.length - 1, g = 0; g <= f; g++) c = g === f ? this : this.clone(!0), r(e[g])[b](c), h.apply(d, c.get()); return this.pushStack(d) } }); var La = /^margin/, Ma = new RegExp("^(" + aa + ")(?!px)[a-z%]+$", "i"), Na = function (b) { var c = b.ownerDocument.defaultView; return c && c.opener || (c = a), c.getComputedStyle(b) }; !function () { function b() { if (i) { i.style.cssText = "box-sizing:border-box;position:relative;display:block;margin:auto;border:1px;padding:1px;top:1%;width:50%", i.innerHTML = "", ra.appendChild(h); var b = a.getComputedStyle(i); c = "1%" !== b.top, g = "2px" === b.marginLeft, e = "4px" === b.width, i.style.marginRight = "50%", f = "4px" === b.marginRight, ra.removeChild(h), i = null } } var c, e, f, g, h = d.createElement("div"), i = d.createElement("div"); i.style && (i.style.backgroundClip = "content-box", i.cloneNode(!0).style.backgroundClip = "", o.clearCloneStyle = "content-box" === i.style.backgroundClip, h.style.cssText = "border:0;width:8px;height:0;top:0;left:-9999px;padding:0;margin-top:1px;position:absolute", h.appendChild(i), r.extend(o, { pixelPosition: function () { return b(), c }, boxSizingReliable: function () { return b(), e }, pixelMarginRight: function () { return b(), f }, reliableMarginLeft: function () { return b(), g } })) }(); function Oa(a, b, c) { var d, e, f, g, h = a.style; return c = c || Na(a), c && (g = c.getPropertyValue(b) || c[b], "" !== g || r.contains(a.ownerDocument, a) || (g = r.style(a, b)), !o.pixelMarginRight() && Ma.test(g) && La.test(b) && (d = h.width, e = h.minWidth, f = h.maxWidth, h.minWidth = h.maxWidth = h.width = g, g = c.width, h.width = d, h.minWidth = e, h.maxWidth = f)), void 0 !== g ? g + "" : g } function Pa(a, b) { return { get: function () { return a() ? void delete this.get : (this.get = b).apply(this, arguments) } } } var Qa = /^(none|table(?!-c[ea]).+)/, Ra = /^--/, Sa = { position: "absolute", visibility: "hidden", display: "block" }, Ta = { letterSpacing: "0", fontWeight: "400" }, Ua = ["Webkit", "Moz", "ms"], Va = d.createElement("div").style; function Wa(a) { if (a in Va) return a; var b = a[0].toUpperCase() + a.slice(1), c = Ua.length; while (c--) if (a = Ua[c] + b, a in Va) return a } function Xa(a) { var b = r.cssProps[a]; return b || (b = r.cssProps[a] = Wa(a) || a), b } function Ya(a, b, c) { var d = ba.exec(b); return d ? Math.max(0, d[2] - (c || 0)) + (d[3] || "px") : b } function Za(a, b, c, d, e) { var f, g = 0; for (f = c === (d ? "border" : "content") ? 4 : "width" === b ? 1 : 0; f < 4; f += 2) "margin" === c && (g += r.css(a, c + ca[f], !0, e)), d ? ("content" === c && (g -= r.css(a, "padding" + ca[f], !0, e)), "margin" !== c && (g -= r.css(a, "border" + ca[f] + "Width", !0, e))) : (g += r.css(a, "padding" + ca[f], !0, e), "padding" !== c && (g += r.css(a, "border" + ca[f] + "Width", !0, e))); return g } function $a(a, b, c) { var d, e = Na(a), f = Oa(a, b, e), g = "border-box" === r.css(a, "boxSizing", !1, e); return Ma.test(f) ? f : (d = g && (o.boxSizingReliable() || f === a.style[b]), "auto" === f && (f = a["offset" + b[0].toUpperCase() + b.slice(1)]), f = parseFloat(f) || 0, f + Za(a, b, c || (g ? "border" : "content"), d, e) + "px") } r.extend({ cssHooks: { opacity: { get: function (a, b) { if (b) { var c = Oa(a, "opacity"); return "" === c ? "1" : c } } } }, cssNumber: { animationIterationCount: !0, columnCount: !0, fillOpacity: !0, flexGrow: !0, flexShrink: !0, fontWeight: !0, lineHeight: !0, opacity: !0, order: !0, orphans: !0, widows: !0, zIndex: !0, zoom: !0 }, cssProps: { "float": "cssFloat" }, style: function (a, b, c, d) { if (a && 3 !== a.nodeType && 8 !== a.nodeType && a.style) { var e, f, g, h = r.camelCase(b), i = Ra.test(b), j = a.style; return i || (b = Xa(h)), g = r.cssHooks[b] || r.cssHooks[h], void 0 === c ? g && "get" in g && void 0 !== (e = g.get(a, !1, d)) ? e : j[b] : (f = typeof c, "string" === f && (e = ba.exec(c)) && e[1] && (c = fa(a, b, e), f = "number"), null != c && c === c && ("number" === f && (c += e && e[3] || (r.cssNumber[h] ? "" : "px")), o.clearCloneStyle || "" !== c || 0 !== b.indexOf("background") || (j[b] = "inherit"), g && "set" in g && void 0 === (c = g.set(a, c, d)) || (i ? j.setProperty(b, c) : j[b] = c)), void 0) } }, css: function (a, b, c, d) { var e, f, g, h = r.camelCase(b), i = Ra.test(b); return i || (b = Xa(h)), g = r.cssHooks[b] || r.cssHooks[h], g && "get" in g && (e = g.get(a, !0, c)), void 0 === e && (e = Oa(a, b, d)), "normal" === e && b in Ta && (e = Ta[b]), "" === c || c ? (f = parseFloat(e), c === !0 || isFinite(f) ? f || 0 : e) : e } }), r.each(["height", "width"], function (a, b) { r.cssHooks[b] = { get: function (a, c, d) { if (c) return !Qa.test(r.css(a, "display")) || a.getClientRects().length && a.getBoundingClientRect().width ? $a(a, b, d) : ea(a, Sa, function () { return $a(a, b, d) }) }, set: function (a, c, d) { var e, f = d && Na(a), g = d && Za(a, b, d, "border-box" === r.css(a, "boxSizing", !1, f), f); return g && (e = ba.exec(c)) && "px" !== (e[3] || "px") && (a.style[b] = c, c = r.css(a, b)), Ya(a, c, g) } } }), r.cssHooks.marginLeft = Pa(o.reliableMarginLeft, function (a, b) { if (b) return (parseFloat(Oa(a, "marginLeft")) || a.getBoundingClientRect().left - ea(a, { marginLeft: 0 }, function () { return a.getBoundingClientRect().left })) + "px" }), r.each({ margin: "", padding: "", border: "Width" }, function (a, b) { r.cssHooks[a + b] = { expand: function (c) { for (var d = 0, e = {}, f = "string" == typeof c ? c.split(" ") : [c]; d < 4; d++) e[a + ca[d] + b] = f[d] || f[d - 2] || f[0]; return e } }, La.test(a) || (r.cssHooks[a + b].set = Ya) }), r.fn.extend({ css: function (a, b) { return T(this, function (a, b, c) { var d, e, f = {}, g = 0; if (Array.isArray(b)) { for (d = Na(a), e = b.length; g < e; g++) f[b[g]] = r.css(a, b[g], !1, d); return f } return void 0 !== c ? r.style(a, b, c) : r.css(a, b) }, a, b, arguments.length > 1) } }); function _a(a, b, c, d, e) { return new _a.prototype.init(a, b, c, d, e) } r.Tween = _a, _a.prototype = { constructor: _a, init: function (a, b, c, d, e, f) { this.elem = a, this.prop = c, this.easing = e || r.easing._default, this.options = b, this.start = this.now = this.cur(), this.end = d, this.unit = f || (r.cssNumber[c] ? "" : "px") }, cur: function () { var a = _a.propHooks[this.prop]; return a && a.get ? a.get(this) : _a.propHooks._default.get(this) }, run: function (a) { var b, c = _a.propHooks[this.prop]; return this.options.duration ? this.pos = b = r.easing[this.easing](a, this.options.duration * a, 0, 1, this.options.duration) : this.pos = b = a, this.now = (this.end - this.start) * b + this.start, this.options.step && this.options.step.call(this.elem, this.now, this), c && c.set ? c.set(this) : _a.propHooks._default.set(this), this } }, _a.prototype.init.prototype = _a.prototype, _a.propHooks = { _default: { get: function (a) { var b; return 1 !== a.elem.nodeType || null != a.elem[a.prop] && null == a.elem.style[a.prop] ? a.elem[a.prop] : (b = r.css(a.elem, a.prop, ""), b && "auto" !== b ? b : 0) }, set: function (a) { r.fx.step[a.prop] ? r.fx.step[a.prop](a) : 1 !== a.elem.nodeType || null == a.elem.style[r.cssProps[a.prop]] && !r.cssHooks[a.prop] ? a.elem[a.prop] = a.now : r.style(a.elem, a.prop, a.now + a.unit) } } }, _a.propHooks.scrollTop = _a.propHooks.scrollLeft = { set: function (a) { a.elem.nodeType && a.elem.parentNode && (a.elem[a.prop] = a.now) } }, r.easing = { linear: function (a) { return a }, swing: function (a) { return .5 - Math.cos(a * Math.PI) / 2 }, _default: "swing" }, r.fx = _a.prototype.init, r.fx.step = {}; var ab, bb, cb = /^(?:toggle|show|hide)$/, db = /queueHooks$/; function eb() { bb && (d.hidden === !1 && a.requestAnimationFrame ? a.requestAnimationFrame(eb) : a.setTimeout(eb, r.fx.interval), r.fx.tick()) } function fb() { return a.setTimeout(function () { ab = void 0 }), ab = r.now() } function gb(a, b) { var c, d = 0, e = { height: a }; for (b = b ? 1 : 0; d < 4; d += 2 - b) c = ca[d], e["margin" + c] = e["padding" + c] = a; return b && (e.opacity = e.width = a), e } function hb(a, b, c) { for (var d, e = (kb.tweeners[b] || []).concat(kb.tweeners["*"]), f = 0, g = e.length; f < g; f++) if (d = e[f].call(c, b, a)) return d } function ib(a, b, c) { var d, e, f, g, h, i, j, k, l = "width" in b || "height" in b, m = this, n = {}, o = a.style, p = a.nodeType && da(a), q = W.get(a, "fxshow"); c.queue || (g = r._queueHooks(a, "fx"), null == g.unqueued && (g.unqueued = 0, h = g.empty.fire, g.empty.fire = function () { g.unqueued || h() }), g.unqueued++ , m.always(function () { m.always(function () { g.unqueued-- , r.queue(a, "fx").length || g.empty.fire() }) })); for (d in b) if (e = b[d], cb.test(e)) { if (delete b[d], f = f || "toggle" === e, e === (p ? "hide" : "show")) { if ("show" !== e || !q || void 0 === q[d]) continue; p = !0 } n[d] = q && q[d] || r.style(a, d) } if (i = !r.isEmptyObject(b), i || !r.isEmptyObject(n)) { l && 1 === a.nodeType && (c.overflow = [o.overflow, o.overflowX, o.overflowY], j = q && q.display, null == j && (j = W.get(a, "display")), k = r.css(a, "display"), "none" === k && (j ? k = j : (ia([a], !0), j = a.style.display || j, k = r.css(a, "display"), ia([a]))), ("inline" === k || "inline-block" === k && null != j) && "none" === r.css(a, "float") && (i || (m.done(function () { o.display = j }), null == j && (k = o.display, j = "none" === k ? "" : k)), o.display = "inline-block")), c.overflow && (o.overflow = "hidden", m.always(function () { o.overflow = c.overflow[0], o.overflowX = c.overflow[1], o.overflowY = c.overflow[2] })), i = !1; for (d in n) i || (q ? "hidden" in q && (p = q.hidden) : q = W.access(a, "fxshow", { display: j }), f && (q.hidden = !p), p && ia([a], !0), m.done(function () { p || ia([a]), W.remove(a, "fxshow"); for (d in n) r.style(a, d, n[d]) })), i = hb(p ? q[d] : 0, d, m), d in q || (q[d] = i.start, p && (i.end = i.start, i.start = 0)) } } function jb(a, b) { var c, d, e, f, g; for (c in a) if (d = r.camelCase(c), e = b[d], f = a[c], Array.isArray(f) && (e = f[1], f = a[c] = f[0]), c !== d && (a[d] = f, delete a[c]), g = r.cssHooks[d], g && "expand" in g) { f = g.expand(f), delete a[d]; for (c in f) c in a || (a[c] = f[c], b[c] = e) } else b[d] = e } function kb(a, b, c) { var d, e, f = 0, g = kb.prefilters.length, h = r.Deferred().always(function () { delete i.elem }), i = function () { if (e) return !1; for (var b = ab || fb(), c = Math.max(0, j.startTime + j.duration - b), d = c / j.duration || 0, f = 1 - d, g = 0, i = j.tweens.length; g < i; g++) j.tweens[g].run(f); return h.notifyWith(a, [j, f, c]), f < 1 && i ? c : (i || h.notifyWith(a, [j, 1, 0]), h.resolveWith(a, [j]), !1) }, j = h.promise({ elem: a, props: r.extend({}, b), opts: r.extend(!0, { specialEasing: {}, easing: r.easing._default }, c), originalProperties: b, originalOptions: c, startTime: ab || fb(), duration: c.duration, tweens: [], createTween: function (b, c) { var d = r.Tween(a, j.opts, b, c, j.opts.specialEasing[b] || j.opts.easing); return j.tweens.push(d), d }, stop: function (b) { var c = 0, d = b ? j.tweens.length : 0; if (e) return this; for (e = !0; c < d; c++) j.tweens[c].run(1); return b ? (h.notifyWith(a, [j, 1, 0]), h.resolveWith(a, [j, b])) : h.rejectWith(a, [j, b]), this } }), k = j.props; for (jb(k, j.opts.specialEasing); f < g; f++) if (d = kb.prefilters[f].call(j, a, k, j.opts)) return r.isFunction(d.stop) && (r._queueHooks(j.elem, j.opts.queue).stop = r.proxy(d.stop, d)), d; return r.map(k, hb, j), r.isFunction(j.opts.start) && j.opts.start.call(a, j), j.progress(j.opts.progress).done(j.opts.done, j.opts.complete).fail(j.opts.fail).always(j.opts.always), r.fx.timer(r.extend(i, { elem: a, anim: j, queue: j.opts.queue })), j } r.Animation = r.extend(kb, { tweeners: { "*": [function (a, b) { var c = this.createTween(a, b); return fa(c.elem, a, ba.exec(b), c), c }] }, tweener: function (a, b) { r.isFunction(a) ? (b = a, a = ["*"]) : a = a.match(L); for (var c, d = 0, e = a.length; d < e; d++) c = a[d], kb.tweeners[c] = kb.tweeners[c] || [], kb.tweeners[c].unshift(b) }, prefilters: [ib], prefilter: function (a, b) { b ? kb.prefilters.unshift(a) : kb.prefilters.push(a) } }), r.speed = function (a, b, c) { var d = a && "object" == typeof a ? r.extend({}, a) : { complete: c || !c && b || r.isFunction(a) && a, duration: a, easing: c && b || b && !r.isFunction(b) && b }; return r.fx.off ? d.duration = 0 : "number" != typeof d.duration && (d.duration in r.fx.speeds ? d.duration = r.fx.speeds[d.duration] : d.duration = r.fx.speeds._default), null != d.queue && d.queue !== !0 || (d.queue = "fx"), d.old = d.complete, d.complete = function () { r.isFunction(d.old) && d.old.call(this), d.queue && r.dequeue(this, d.queue) }, d }, r.fn.extend({ fadeTo: function (a, b, c, d) { return this.filter(da).css("opacity", 0).show().end().animate({ opacity: b }, a, c, d) }, animate: function (a, b, c, d) { var e = r.isEmptyObject(a), f = r.speed(b, c, d), g = function () { var b = kb(this, r.extend({}, a), f); (e || W.get(this, "finish")) && b.stop(!0) }; return g.finish = g, e || f.queue === !1 ? this.each(g) : this.queue(f.queue, g) }, stop: function (a, b, c) { var d = function (a) { var b = a.stop; delete a.stop, b(c) }; return "string" != typeof a && (c = b, b = a, a = void 0), b && a !== !1 && this.queue(a || "fx", []), this.each(function () { var b = !0, e = null != a && a + "queueHooks", f = r.timers, g = W.get(this); if (e) g[e] && g[e].stop && d(g[e]); else for (e in g) g[e] && g[e].stop && db.test(e) && d(g[e]); for (e = f.length; e--;) f[e].elem !== this || null != a && f[e].queue !== a || (f[e].anim.stop(c), b = !1, f.splice(e, 1)); !b && c || r.dequeue(this, a) }) }, finish: function (a) { return a !== !1 && (a = a || "fx"), this.each(function () { var b, c = W.get(this), d = c[a + "queue"], e = c[a + "queueHooks"], f = r.timers, g = d ? d.length : 0; for (c.finish = !0, r.queue(this, a, []), e && e.stop && e.stop.call(this, !0), b = f.length; b--;) f[b].elem === this && f[b].queue === a && (f[b].anim.stop(!0), f.splice(b, 1)); for (b = 0; b < g; b++) d[b] && d[b].finish && d[b].finish.call(this); delete c.finish }) } }), r.each(["toggle", "show", "hide"], function (a, b) { var c = r.fn[b]; r.fn[b] = function (a, d, e) { return null == a || "boolean" == typeof a ? c.apply(this, arguments) : this.animate(gb(b, !0), a, d, e) } }), r.each({ slideDown: gb("show"), slideUp: gb("hide"), slideToggle: gb("toggle"), fadeIn: { opacity: "show" }, fadeOut: { opacity: "hide" }, fadeToggle: { opacity: "toggle" } }, function (a, b) { r.fn[a] = function (a, c, d) { return this.animate(b, a, c, d) } }), r.timers = [], r.fx.tick = function () { var a, b = 0, c = r.timers; for (ab = r.now(); b < c.length; b++) a = c[b], a() || c[b] !== a || c.splice(b--, 1); c.length || r.fx.stop(), ab = void 0 }, r.fx.timer = function (a) { r.timers.push(a), r.fx.start() }, r.fx.interval = 13, r.fx.start = function () { bb || (bb = !0, eb()) }, r.fx.stop = function () { bb = null }, r.fx.speeds = { slow: 600, fast: 200, _default: 400 }, r.fn.delay = function (b, c) { return b = r.fx ? r.fx.speeds[b] || b : b, c = c || "fx", this.queue(c, function (c, d) { var e = a.setTimeout(c, b); d.stop = function () { a.clearTimeout(e) } }) }, function () { var a = d.createElement("input"), b = d.createElement("select"), c = b.appendChild(d.createElement("option")); a.type = "checkbox", o.checkOn = "" !== a.value, o.optSelected = c.selected, a = d.createElement("input"), a.value = "t", a.type = "radio", o.radioValue = "t" === a.value }(); var lb, mb = r.expr.attrHandle; r.fn.extend({ attr: function (a, b) { return T(this, r.attr, a, b, arguments.length > 1) }, removeAttr: function (a) { return this.each(function () { r.removeAttr(this, a) }) } }), r.extend({ + attr: function (a, b, c) { + var d, e, f = a.nodeType; if (3 !== f && 8 !== f && 2 !== f) return "undefined" == typeof a.getAttribute ? r.prop(a, b, c) : (1 === f && r.isXMLDoc(a) || (e = r.attrHooks[b.toLowerCase()] || (r.expr.match.bool.test(b) ? lb : void 0)), void 0 !== c ? null === c ? void r.removeAttr(a, b) : e && "set" in e && void 0 !== (d = e.set(a, c, b)) ? d : (a.setAttribute(b, c + ""), c) : e && "get" in e && null !== (d = e.get(a, b)) ? d : (d = r.find.attr(a, b), + null == d ? void 0 : d)) + }, attrHooks: { type: { set: function (a, b) { if (!o.radioValue && "radio" === b && B(a, "input")) { var c = a.value; return a.setAttribute("type", b), c && (a.value = c), b } } } }, removeAttr: function (a, b) { var c, d = 0, e = b && b.match(L); if (e && 1 === a.nodeType) while (c = e[d++]) a.removeAttribute(c) } + }), lb = { set: function (a, b, c) { return b === !1 ? r.removeAttr(a, c) : a.setAttribute(c, c), c } }, r.each(r.expr.match.bool.source.match(/\w+/g), function (a, b) { var c = mb[b] || r.find.attr; mb[b] = function (a, b, d) { var e, f, g = b.toLowerCase(); return d || (f = mb[g], mb[g] = e, e = null != c(a, b, d) ? g : null, mb[g] = f), e } }); var nb = /^(?:input|select|textarea|button)$/i, ob = /^(?:a|area)$/i; r.fn.extend({ prop: function (a, b) { return T(this, r.prop, a, b, arguments.length > 1) }, removeProp: function (a) { return this.each(function () { delete this[r.propFix[a] || a] }) } }), r.extend({ prop: function (a, b, c) { var d, e, f = a.nodeType; if (3 !== f && 8 !== f && 2 !== f) return 1 === f && r.isXMLDoc(a) || (b = r.propFix[b] || b, e = r.propHooks[b]), void 0 !== c ? e && "set" in e && void 0 !== (d = e.set(a, c, b)) ? d : a[b] = c : e && "get" in e && null !== (d = e.get(a, b)) ? d : a[b] }, propHooks: { tabIndex: { get: function (a) { var b = r.find.attr(a, "tabindex"); return b ? parseInt(b, 10) : nb.test(a.nodeName) || ob.test(a.nodeName) && a.href ? 0 : -1 } } }, propFix: { "for": "htmlFor", "class": "className" } }), o.optSelected || (r.propHooks.selected = { get: function (a) { var b = a.parentNode; return b && b.parentNode && b.parentNode.selectedIndex, null }, set: function (a) { var b = a.parentNode; b && (b.selectedIndex, b.parentNode && b.parentNode.selectedIndex) } }), r.each(["tabIndex", "readOnly", "maxLength", "cellSpacing", "cellPadding", "rowSpan", "colSpan", "useMap", "frameBorder", "contentEditable"], function () { r.propFix[this.toLowerCase()] = this }); function pb(a) { var b = a.match(L) || []; return b.join(" ") } function qb(a) { return a.getAttribute && a.getAttribute("class") || "" } r.fn.extend({ addClass: function (a) { var b, c, d, e, f, g, h, i = 0; if (r.isFunction(a)) return this.each(function (b) { r(this).addClass(a.call(this, b, qb(this))) }); if ("string" == typeof a && a) { b = a.match(L) || []; while (c = this[i++]) if (e = qb(c), d = 1 === c.nodeType && " " + pb(e) + " ") { g = 0; while (f = b[g++]) d.indexOf(" " + f + " ") < 0 && (d += f + " "); h = pb(d), e !== h && c.setAttribute("class", h) } } return this }, removeClass: function (a) { var b, c, d, e, f, g, h, i = 0; if (r.isFunction(a)) return this.each(function (b) { r(this).removeClass(a.call(this, b, qb(this))) }); if (!arguments.length) return this.attr("class", ""); if ("string" == typeof a && a) { b = a.match(L) || []; while (c = this[i++]) if (e = qb(c), d = 1 === c.nodeType && " " + pb(e) + " ") { g = 0; while (f = b[g++]) while (d.indexOf(" " + f + " ") > -1) d = d.replace(" " + f + " ", " "); h = pb(d), e !== h && c.setAttribute("class", h) } } return this }, toggleClass: function (a, b) { var c = typeof a; return "boolean" == typeof b && "string" === c ? b ? this.addClass(a) : this.removeClass(a) : r.isFunction(a) ? this.each(function (c) { r(this).toggleClass(a.call(this, c, qb(this), b), b) }) : this.each(function () { var b, d, e, f; if ("string" === c) { d = 0, e = r(this), f = a.match(L) || []; while (b = f[d++]) e.hasClass(b) ? e.removeClass(b) : e.addClass(b) } else void 0 !== a && "boolean" !== c || (b = qb(this), b && W.set(this, "__className__", b), this.setAttribute && this.setAttribute("class", b || a === !1 ? "" : W.get(this, "__className__") || "")) }) }, hasClass: function (a) { var b, c, d = 0; b = " " + a + " "; while (c = this[d++]) if (1 === c.nodeType && (" " + pb(qb(c)) + " ").indexOf(b) > -1) return !0; return !1 } }); var rb = /\r/g; r.fn.extend({ val: function (a) { var b, c, d, e = this[0]; { if (arguments.length) return d = r.isFunction(a), this.each(function (c) { var e; 1 === this.nodeType && (e = d ? a.call(this, c, r(this).val()) : a, null == e ? e = "" : "number" == typeof e ? e += "" : Array.isArray(e) && (e = r.map(e, function (a) { return null == a ? "" : a + "" })), b = r.valHooks[this.type] || r.valHooks[this.nodeName.toLowerCase()], b && "set" in b && void 0 !== b.set(this, e, "value") || (this.value = e)) }); if (e) return b = r.valHooks[e.type] || r.valHooks[e.nodeName.toLowerCase()], b && "get" in b && void 0 !== (c = b.get(e, "value")) ? c : (c = e.value, "string" == typeof c ? c.replace(rb, "") : null == c ? "" : c) } } }), r.extend({ valHooks: { option: { get: function (a) { var b = r.find.attr(a, "value"); return null != b ? b : pb(r.text(a)) } }, select: { get: function (a) { var b, c, d, e = a.options, f = a.selectedIndex, g = "select-one" === a.type, h = g ? null : [], i = g ? f + 1 : e.length; for (d = f < 0 ? i : g ? f : 0; d < i; d++) if (c = e[d], (c.selected || d === f) && !c.disabled && (!c.parentNode.disabled || !B(c.parentNode, "optgroup"))) { if (b = r(c).val(), g) return b; h.push(b) } return h }, set: function (a, b) { var c, d, e = a.options, f = r.makeArray(b), g = e.length; while (g--) d = e[g], (d.selected = r.inArray(r.valHooks.option.get(d), f) > -1) && (c = !0); return c || (a.selectedIndex = -1), f } } } }), r.each(["radio", "checkbox"], function () { r.valHooks[this] = { set: function (a, b) { if (Array.isArray(b)) return a.checked = r.inArray(r(a).val(), b) > -1 } }, o.checkOn || (r.valHooks[this].get = function (a) { return null === a.getAttribute("value") ? "on" : a.value }) }); var sb = /^(?:focusinfocus|focusoutblur)$/; r.extend(r.event, { trigger: function (b, c, e, f) { var g, h, i, j, k, m, n, o = [e || d], p = l.call(b, "type") ? b.type : b, q = l.call(b, "namespace") ? b.namespace.split(".") : []; if (h = i = e = e || d, 3 !== e.nodeType && 8 !== e.nodeType && !sb.test(p + r.event.triggered) && (p.indexOf(".") > -1 && (q = p.split("."), p = q.shift(), q.sort()), k = p.indexOf(":") < 0 && "on" + p, b = b[r.expando] ? b : new r.Event(p, "object" == typeof b && b), b.isTrigger = f ? 2 : 3, b.namespace = q.join("."), b.rnamespace = b.namespace ? new RegExp("(^|\\.)" + q.join("\\.(?:.*\\.|)") + "(\\.|$)") : null, b.result = void 0, b.target || (b.target = e), c = null == c ? [b] : r.makeArray(c, [b]), n = r.event.special[p] || {}, f || !n.trigger || n.trigger.apply(e, c) !== !1)) { if (!f && !n.noBubble && !r.isWindow(e)) { for (j = n.delegateType || p, sb.test(j + p) || (h = h.parentNode); h; h = h.parentNode) o.push(h), i = h; i === (e.ownerDocument || d) && o.push(i.defaultView || i.parentWindow || a) } g = 0; while ((h = o[g++]) && !b.isPropagationStopped()) b.type = g > 1 ? j : n.bindType || p, m = (W.get(h, "events") || {})[b.type] && W.get(h, "handle"), m && m.apply(h, c), m = k && h[k], m && m.apply && U(h) && (b.result = m.apply(h, c), b.result === !1 && b.preventDefault()); return b.type = p, f || b.isDefaultPrevented() || n._default && n._default.apply(o.pop(), c) !== !1 || !U(e) || k && r.isFunction(e[p]) && !r.isWindow(e) && (i = e[k], i && (e[k] = null), r.event.triggered = p, e[p](), r.event.triggered = void 0, i && (e[k] = i)), b.result } }, simulate: function (a, b, c) { var d = r.extend(new r.Event, c, { type: a, isSimulated: !0 }); r.event.trigger(d, null, b) } }), r.fn.extend({ trigger: function (a, b) { return this.each(function () { r.event.trigger(a, b, this) }) }, triggerHandler: function (a, b) { var c = this[0]; if (c) return r.event.trigger(a, b, c, !0) } }), r.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "), function (a, b) { r.fn[b] = function (a, c) { return arguments.length > 0 ? this.on(b, null, a, c) : this.trigger(b) } }), r.fn.extend({ hover: function (a, b) { return this.mouseenter(a).mouseleave(b || a) } }), o.focusin = "onfocusin" in a, o.focusin || r.each({ focus: "focusin", blur: "focusout" }, function (a, b) { var c = function (a) { r.event.simulate(b, a.target, r.event.fix(a)) }; r.event.special[b] = { setup: function () { var d = this.ownerDocument || this, e = W.access(d, b); e || d.addEventListener(a, c, !0), W.access(d, b, (e || 0) + 1) }, teardown: function () { var d = this.ownerDocument || this, e = W.access(d, b) - 1; e ? W.access(d, b, e) : (d.removeEventListener(a, c, !0), W.remove(d, b)) } } }); var tb = a.location, ub = r.now(), vb = /\?/; r.parseXML = function (b) { var c; if (!b || "string" != typeof b) return null; try { c = (new a.DOMParser).parseFromString(b, "text/xml") } catch (d) { c = void 0 } return c && !c.getElementsByTagName("parsererror").length || r.error("Invalid XML: " + b), c }; var wb = /\[\]$/, xb = /\r?\n/g, yb = /^(?:submit|button|image|reset|file)$/i, zb = /^(?:input|select|textarea|keygen)/i; function Ab(a, b, c, d) { var e; if (Array.isArray(b)) r.each(b, function (b, e) { c || wb.test(a) ? d(a, e) : Ab(a + "[" + ("object" == typeof e && null != e ? b : "") + "]", e, c, d) }); else if (c || "object" !== r.type(b)) d(a, b); else for (e in b) Ab(a + "[" + e + "]", b[e], c, d) } r.param = function (a, b) { var c, d = [], e = function (a, b) { var c = r.isFunction(b) ? b() : b; d[d.length] = encodeURIComponent(a) + "=" + encodeURIComponent(null == c ? "" : c) }; if (Array.isArray(a) || a.jquery && !r.isPlainObject(a)) r.each(a, function () { e(this.name, this.value) }); else for (c in a) Ab(c, a[c], b, e); return d.join("&") }, r.fn.extend({ serialize: function () { return r.param(this.serializeArray()) }, serializeArray: function () { return this.map(function () { var a = r.prop(this, "elements"); return a ? r.makeArray(a) : this }).filter(function () { var a = this.type; return this.name && !r(this).is(":disabled") && zb.test(this.nodeName) && !yb.test(a) && (this.checked || !ja.test(a)) }).map(function (a, b) { var c = r(this).val(); return null == c ? null : Array.isArray(c) ? r.map(c, function (a) { return { name: b.name, value: a.replace(xb, "\r\n") } }) : { name: b.name, value: c.replace(xb, "\r\n") } }).get() } }); var Bb = /%20/g, Cb = /#.*$/, Db = /([?&])_=[^&]*/, Eb = /^(.*?):[ \t]*([^\r\n]*)$/gm, Fb = /^(?:about|app|app-storage|.+-extension|file|res|widget):$/, Gb = /^(?:GET|HEAD)$/, Hb = /^\/\//, Ib = {}, Jb = {}, Kb = "*/".concat("*"), Lb = d.createElement("a"); Lb.href = tb.href; function Mb(a) { return function (b, c) { "string" != typeof b && (c = b, b = "*"); var d, e = 0, f = b.toLowerCase().match(L) || []; if (r.isFunction(c)) while (d = f[e++]) "+" === d[0] ? (d = d.slice(1) || "*", (a[d] = a[d] || []).unshift(c)) : (a[d] = a[d] || []).push(c) } } function Nb(a, b, c, d) { var e = {}, f = a === Jb; function g(h) { var i; return e[h] = !0, r.each(a[h] || [], function (a, h) { var j = h(b, c, d); return "string" != typeof j || f || e[j] ? f ? !(i = j) : void 0 : (b.dataTypes.unshift(j), g(j), !1) }), i } return g(b.dataTypes[0]) || !e["*"] && g("*") } function Ob(a, b) { var c, d, e = r.ajaxSettings.flatOptions || {}; for (c in b) void 0 !== b[c] && ((e[c] ? a : d || (d = {}))[c] = b[c]); return d && r.extend(!0, a, d), a } function Pb(a, b, c) { var d, e, f, g, h = a.contents, i = a.dataTypes; while ("*" === i[0]) i.shift(), void 0 === d && (d = a.mimeType || b.getResponseHeader("Content-Type")); if (d) for (e in h) if (h[e] && h[e].test(d)) { i.unshift(e); break } if (i[0] in c) f = i[0]; else { for (e in c) { if (!i[0] || a.converters[e + " " + i[0]]) { f = e; break } g || (g = e) } f = f || g } if (f) return f !== i[0] && i.unshift(f), c[f] } function Qb(a, b, c, d) { var e, f, g, h, i, j = {}, k = a.dataTypes.slice(); if (k[1]) for (g in a.converters) j[g.toLowerCase()] = a.converters[g]; f = k.shift(); while (f) if (a.responseFields[f] && (c[a.responseFields[f]] = b), !i && d && a.dataFilter && (b = a.dataFilter(b, a.dataType)), i = f, f = k.shift()) if ("*" === f) f = i; else if ("*" !== i && i !== f) { if (g = j[i + " " + f] || j["* " + f], !g) for (e in j) if (h = e.split(" "), h[1] === f && (g = j[i + " " + h[0]] || j["* " + h[0]])) { g === !0 ? g = j[e] : j[e] !== !0 && (f = h[0], k.unshift(h[1])); break } if (g !== !0) if (g && a["throws"]) b = g(b); else try { b = g(b) } catch (l) { return { state: "parsererror", error: g ? l : "No conversion from " + i + " to " + f } } } return { state: "success", data: b } } r.extend({ active: 0, lastModified: {}, etag: {}, ajaxSettings: { url: tb.href, type: "GET", isLocal: Fb.test(tb.protocol), global: !0, processData: !0, async: !0, contentType: "application/x-www-form-urlencoded; charset=UTF-8", accepts: { "*": Kb, text: "text/plain", html: "text/html", xml: "application/xml, text/xml", json: "application/json, text/javascript" }, contents: { xml: /\bxml\b/, html: /\bhtml/, json: /\bjson\b/ }, responseFields: { xml: "responseXML", text: "responseText", json: "responseJSON" }, converters: { "* text": String, "text html": !0, "text json": JSON.parse, "text xml": r.parseXML }, flatOptions: { url: !0, context: !0 } }, ajaxSetup: function (a, b) { return b ? Ob(Ob(a, r.ajaxSettings), b) : Ob(r.ajaxSettings, a) }, ajaxPrefilter: Mb(Ib), ajaxTransport: Mb(Jb), ajax: function (b, c) { "object" == typeof b && (c = b, b = void 0), c = c || {}; var e, f, g, h, i, j, k, l, m, n, o = r.ajaxSetup({}, c), p = o.context || o, q = o.context && (p.nodeType || p.jquery) ? r(p) : r.event, s = r.Deferred(), t = r.Callbacks("once memory"), u = o.statusCode || {}, v = {}, w = {}, x = "canceled", y = { readyState: 0, getResponseHeader: function (a) { var b; if (k) { if (!h) { h = {}; while (b = Eb.exec(g)) h[b[1].toLowerCase()] = b[2] } b = h[a.toLowerCase()] } return null == b ? null : b }, getAllResponseHeaders: function () { return k ? g : null }, setRequestHeader: function (a, b) { return null == k && (a = w[a.toLowerCase()] = w[a.toLowerCase()] || a, v[a] = b), this }, overrideMimeType: function (a) { return null == k && (o.mimeType = a), this }, statusCode: function (a) { var b; if (a) if (k) y.always(a[y.status]); else for (b in a) u[b] = [u[b], a[b]]; return this }, abort: function (a) { var b = a || x; return e && e.abort(b), A(0, b), this } }; if (s.promise(y), o.url = ((b || o.url || tb.href) + "").replace(Hb, tb.protocol + "//"), o.type = c.method || c.type || o.method || o.type, o.dataTypes = (o.dataType || "*").toLowerCase().match(L) || [""], null == o.crossDomain) { j = d.createElement("a"); try { j.href = o.url, j.href = j.href, o.crossDomain = Lb.protocol + "//" + Lb.host != j.protocol + "//" + j.host } catch (z) { o.crossDomain = !0 } } if (o.data && o.processData && "string" != typeof o.data && (o.data = r.param(o.data, o.traditional)), Nb(Ib, o, c, y), k) return y; l = r.event && o.global, l && 0 === r.active++ && r.event.trigger("ajaxStart"), o.type = o.type.toUpperCase(), o.hasContent = !Gb.test(o.type), f = o.url.replace(Cb, ""), o.hasContent ? o.data && o.processData && 0 === (o.contentType || "").indexOf("application/x-www-form-urlencoded") && (o.data = o.data.replace(Bb, "+")) : (n = o.url.slice(f.length), o.data && (f += (vb.test(f) ? "&" : "?") + o.data, delete o.data), o.cache === !1 && (f = f.replace(Db, "$1"), n = (vb.test(f) ? "&" : "?") + "_=" + ub++ + n), o.url = f + n), o.ifModified && (r.lastModified[f] && y.setRequestHeader("If-Modified-Since", r.lastModified[f]), r.etag[f] && y.setRequestHeader("If-None-Match", r.etag[f])), (o.data && o.hasContent && o.contentType !== !1 || c.contentType) && y.setRequestHeader("Content-Type", o.contentType), y.setRequestHeader("Accept", o.dataTypes[0] && o.accepts[o.dataTypes[0]] ? o.accepts[o.dataTypes[0]] + ("*" !== o.dataTypes[0] ? ", " + Kb + "; q=0.01" : "") : o.accepts["*"]); for (m in o.headers) y.setRequestHeader(m, o.headers[m]); if (o.beforeSend && (o.beforeSend.call(p, y, o) === !1 || k)) return y.abort(); if (x = "abort", t.add(o.complete), y.done(o.success), y.fail(o.error), e = Nb(Jb, o, c, y)) { if (y.readyState = 1, l && q.trigger("ajaxSend", [y, o]), k) return y; o.async && o.timeout > 0 && (i = a.setTimeout(function () { y.abort("timeout") }, o.timeout)); try { k = !1, e.send(v, A) } catch (z) { if (k) throw z; A(-1, z) } } else A(-1, "No Transport"); function A(b, c, d, h) { var j, m, n, v, w, x = c; k || (k = !0, i && a.clearTimeout(i), e = void 0, g = h || "", y.readyState = b > 0 ? 4 : 0, j = b >= 200 && b < 300 || 304 === b, d && (v = Pb(o, y, d)), v = Qb(o, v, y, j), j ? (o.ifModified && (w = y.getResponseHeader("Last-Modified"), w && (r.lastModified[f] = w), w = y.getResponseHeader("etag"), w && (r.etag[f] = w)), 204 === b || "HEAD" === o.type ? x = "nocontent" : 304 === b ? x = "notmodified" : (x = v.state, m = v.data, n = v.error, j = !n)) : (n = x, !b && x || (x = "error", b < 0 && (b = 0))), y.status = b, y.statusText = (c || x) + "", j ? s.resolveWith(p, [m, x, y]) : s.rejectWith(p, [y, x, n]), y.statusCode(u), u = void 0, l && q.trigger(j ? "ajaxSuccess" : "ajaxError", [y, o, j ? m : n]), t.fireWith(p, [y, x]), l && (q.trigger("ajaxComplete", [y, o]), --r.active || r.event.trigger("ajaxStop"))) } return y }, getJSON: function (a, b, c) { return r.get(a, b, c, "json") }, getScript: function (a, b) { return r.get(a, void 0, b, "script") } }), r.each(["get", "post"], function (a, b) { r[b] = function (a, c, d, e) { return r.isFunction(c) && (e = e || d, d = c, c = void 0), r.ajax(r.extend({ url: a, type: b, dataType: e, data: c, success: d }, r.isPlainObject(a) && a)) } }), r._evalUrl = function (a) { return r.ajax({ url: a, type: "GET", dataType: "script", cache: !0, async: !1, global: !1, "throws": !0 }) }, r.fn.extend({ wrapAll: function (a) { var b; return this[0] && (r.isFunction(a) && (a = a.call(this[0])), b = r(a, this[0].ownerDocument).eq(0).clone(!0), this[0].parentNode && b.insertBefore(this[0]), b.map(function () { var a = this; while (a.firstElementChild) a = a.firstElementChild; return a }).append(this)), this }, wrapInner: function (a) { return r.isFunction(a) ? this.each(function (b) { r(this).wrapInner(a.call(this, b)) }) : this.each(function () { var b = r(this), c = b.contents(); c.length ? c.wrapAll(a) : b.append(a) }) }, wrap: function (a) { var b = r.isFunction(a); return this.each(function (c) { r(this).wrapAll(b ? a.call(this, c) : a) }) }, unwrap: function (a) { return this.parent(a).not("body").each(function () { r(this).replaceWith(this.childNodes) }), this } }), r.expr.pseudos.hidden = function (a) { return !r.expr.pseudos.visible(a) }, r.expr.pseudos.visible = function (a) { return !!(a.offsetWidth || a.offsetHeight || a.getClientRects().length) }, r.ajaxSettings.xhr = function () { try { return new a.XMLHttpRequest } catch (b) { } }; var Rb = { 0: 200, 1223: 204 }, Sb = r.ajaxSettings.xhr(); o.cors = !!Sb && "withCredentials" in Sb, o.ajax = Sb = !!Sb, r.ajaxTransport(function (b) { var c, d; if (o.cors || Sb && !b.crossDomain) return { send: function (e, f) { var g, h = b.xhr(); if (h.open(b.type, b.url, b.async, b.username, b.password), b.xhrFields) for (g in b.xhrFields) h[g] = b.xhrFields[g]; b.mimeType && h.overrideMimeType && h.overrideMimeType(b.mimeType), b.crossDomain || e["X-Requested-With"] || (e["X-Requested-With"] = "XMLHttpRequest"); for (g in e) h.setRequestHeader(g, e[g]); c = function (a) { return function () { c && (c = d = h.onload = h.onerror = h.onabort = h.onreadystatechange = null, "abort" === a ? h.abort() : "error" === a ? "number" != typeof h.status ? f(0, "error") : f(h.status, h.statusText) : f(Rb[h.status] || h.status, h.statusText, "text" !== (h.responseType || "text") || "string" != typeof h.responseText ? { binary: h.response } : { text: h.responseText }, h.getAllResponseHeaders())) } }, h.onload = c(), d = h.onerror = c("error"), void 0 !== h.onabort ? h.onabort = d : h.onreadystatechange = function () { 4 === h.readyState && a.setTimeout(function () { c && d() }) }, c = c("abort"); try { h.send(b.hasContent && b.data || null) } catch (i) { if (c) throw i } }, abort: function () { c && c() } } }), r.ajaxPrefilter(function (a) { a.crossDomain && (a.contents.script = !1) }), r.ajaxSetup({ accepts: { script: "text/javascript, application/javascript, application/ecmascript, application/x-ecmascript" }, contents: { script: /\b(?:java|ecma)script\b/ }, converters: { "text script": function (a) { return r.globalEval(a), a } } }), r.ajaxPrefilter("script", function (a) { void 0 === a.cache && (a.cache = !1), a.crossDomain && (a.type = "GET") }), r.ajaxTransport("script", function (a) { if (a.crossDomain) { var b, c; return { send: function (e, f) { b = r(" + + + + + +
+ +

+ [programName] [programVersion] +

+ + + + + + + +
+
+ + + +
+ +
+ +

Settings

+ +
+ +
+ Wi-Fi + + + + + + + + + + + + + + + + + + + + + + +
Hostname: + +
Wi-Fi SSID: + + +
+ +
Wi-Fi Password: + +
+
+ +
+ +
+ Reverse Beacon Network + + + + + + + +
Callsign: + +
+
+ +
+ +
+ Miscellaneous + + + + + + + + + + + + + + + + + + + +
Friend Cycle Count: + +
LED Enabled: + +
Sound Enabled: + +
Volume: + + +
+
+ +
+ + + + + + +
Update + +
+
+
+
+ + + diff --git a/data/styles.css b/data/styles.css new file mode 100644 index 0000000..0a30fd8 --- /dev/null +++ b/data/styles.css @@ -0,0 +1,333 @@ +body { + font-family: Arial, Helvetica, sans-serif; + font-size: 16px; + margin: 0px; + padding: 0px; +} + +#footer { + position: fixed; + bottom: 0; + height: 18px; + width: 100%; + text-align: center; + color: black; + clear: both; + margin-right: 30px; + margin-top: 30px; +} + +#volumeSlider { + max-width: 100px; +} + +.norm { + width: 95%; + height: 3vh; + font-family: Arial, Helvetica, sans-serif; + margin-top: 5px; + padding-left: 45px; + padding-top: 5px; + border-left-width: 1px; + border-left-style: solid; + border-left-color: #BED6E2; +} + +.Grid { + display: flex; +} + +.Grid-cell { + flex: 1; + min-width:500px; + min-height: 600px; + width: 90vw; + height: 80vh; +} + +.Grid-cell-side { + flex: 0 0 220px; + height: 600; + border: solid 1px grey; + background-color: lightgrey; + font-size: 16px; + } + +.Grid-cell-connection { + flex: 1; + border: solid 1px grey; + background-color: lightgrey; + font-size: 12px; + } + +h1 { + color: black; + font-weight: bolder; + background-repeat: no-repeat; + margin: 10px; + font-size: 22px; +} + +td, th { + color: black; + font-weight: lighter; + background-repeat: no-repeat; + margin: 0px; + font-size: 18px; + border-right: solid 1px white; + padding-right: 13px; + padding-left: 13px; +} + +input[type="color"], +input[type="date"], +input[type="datetime"], +input[type="datetime-local"], +input[type="email"], +input[type="month"], +input[type="number"], +input[type="password"], +input[type="search"], +input[type="tel"], +input[type="text"], +input[type="time"], +input[type="url"], +input[type="week"], +select:focus, +textarea { + font-size: 16px; +} + +input[type="number"] { + -webkit-appearance: none; +} + + +@media screen and (-webkit-min-device-pixel-ratio:0) { + select:focus, + select, + textarea, + input { + font-size: 16px; + } +} + +.value { + border: 0; + text-align: right; + width: 50px; + display: inline-block; +} + +.value-display { + border: 0; + text-align: right; + width: 75px; + display: inline-block; + background-color: lightgrey; +} + +.sweep-display { + border: 0; + text-align: right; + width: 65px; + display: inline-block; + background-color: lightgrey; +} + +.message-display { + border: 0; + text-align: left; + width: 200px; + height: 200px; + display: inline-block; + background-color: lightgrey; +} + + +.value-input { + border: solid 1px grey; + text-align: right; + width: 73px; + display: inline-block; + } + +.checkbox-input { + border: solid 1px grey; + text-align: right; + width: 12px; + display: inline-block; + } + + +.select-input { + border: solid 1px grey; + text-align: right; + text-align-last: right; + width: 77px; + display: inline-block; + } + +.Sweep-select { + border: solid 1px grey; + text-align: left; + text-align-last: left; + width: 150px; + display: inline-block; + color: black; + font-weight: bolder; + background-repeat: no-repeat; + margin: 10px; + } + +.select-option { + text-align: right; + text-align-last: right; +} +.sweep-option { + text-align: left; + text-align-last: left; +} + +.inc-button { + width: 60px; + height: 30px; + font-size: 14px; + float: right; + margin-right:60px; + margin-top:3px; +} + +.on-button { + width: 60px; + height: 33px; + font-size: 14px; + float: right; + margin-right:0px; + margin-top:3px; +} + +.setting { + width: 210px; +} + +.setting-label { + width: 80px; + display: inline-block; + } + +.setting-widelabel { + width: 138px; + display: inline-block; + } + +.sigTitle { + font-size: 36px; + font-weight: bold; +} + +.sigFreq { + font-weight: bolder; + font-size: 32px; + margin: 0; + padding: 0; + letter-spacing: -10px; + width: 500px; +} + +.sigFreqLabel { + width: 180px; + display: inline-block; + font-size: 32px; + margin: 0; + padding: 0; + letter-spacing: normal; +} + +.sigFreqText { + display: inline-block; + font-size: 32px; + margin: 0; + padding: 0; + letter-spacing: normal; +} + +input.sigFreqInput { + border: solid 1px grey; + text-align: right; + width: 18px; + display: inline-block; + font-size: 32px; + margin: 0; + padding: 0; +} + +input.sigFrequencyInput { + border: solid 1px grey; + text-align: right; + width: 180px; + display: inline-block; + font-size: 32px; + margin: 0; + padding: 0; +} + +input.sigFreqInput::-webkit-outer-spin-button, +input.sigFreqInput::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} +input.sigFreqInput { + -moz-appearance: textfield; +} + +.sigLevelLabel { + width: 180px; + display: inline-block; + font-size: 32px; + letter-spacing: normal; + } + +input.sigLevelInput { + border: solid 1px grey; + text-align: right; + width: 180px; + display: inline-block; + font-size: 32px; + } + + +TH, .bold { + background-color: lightblue; + color: black; +} + +.slider { + -webkit-appearance: none; + width: 340px; + height: 15px; + border-radius: 5px; + background: #d3d3d3; + outline: none; + opacity: 0.7; + -webkit-transition: .2s; + transition: opacity .2s; +} + +.slider::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 25px; + height: 25px; + border-radius: 50%; + background: #4CAF50; + cursor: pointer; +} + +.slider::-moz-range-thumb { + width: 25px; + height: 25px; + border-radius: 50%; + background: #4CAF50; + cursor: pointer; +} diff --git a/data/support.js b/data/support.js new file mode 100644 index 0000000..f639e3c --- /dev/null +++ b/data/support.js @@ -0,0 +1,101 @@ +function updateNameVersion() { + + // Get the program name and version + + $.ajax({ + url: "getNameVersion" + }).done(function (data) { + + // We have the data, process it. + + var indexNameVersionETC = data.documentElement; + $("#programName").html(indexNameVersionETC.getAttribute("Name")); + $("#programVersion").html(indexNameVersionETC.getAttribute("Version")); + $("#programCopyright").html(indexNameVersionETC.getAttribute("Copyright")); + + document.title = indexNameVersionETC.getAttribute("Name"); + + }).fail(function (jqXHR, textStatus) { + + alert("Contact with host lost!" + textStatus); + }); +} + +// Home.html specific + +// This method is called to add a number of table headers to a table object. +// It is passed the div into which to draw and it will create the new table +// and return a reference to the associated . + +function buildTable(divToUse, titles) { + + // Clean out the div + + divToUse.empty(); + + // Create the main table objects + + var table = $(""); + var thead = $(""); + var tbody = $(""); + var tr = $(""); + + // Append them together + + tr.appendTo(thead); + thead.appendTo(table); + tbody.appendTo(table); + + // Process and add all the table headers + + for (var i = 0; i < titles.length; i++) { + var th = $("
"); + var span = $(""); + span.html(titles[i]); + span.appendTo(th); + + th.appendTo(tr); + } + + // Append them to the table + + table.appendTo(divToUse); + + // Return the table body + + return tbody; +} + +// Settings.html specific + +function updateSSIDs() { + + // Get the program name and version + + $.ajax({ + url: "getSSIDs" + }).done(function (data) { + + // We have the data, process it. + + $("#availableSSIDS").empty(); + + var ssids = data.documentElement.getElementsByTagName("SSID"); + if (0 === ssids.length) { + alert("No SSIDS visible!"); + } + else { + + for (var i = 0; i < ssids.length; i++) { + + var ssid = ssids[i]; + + $("