commit 87309628fc2b7d83730b0522975b899c0cd685bb Author: caiyu <290198250@qq.com> Date: Sat Feb 22 18:13:51 2025 +0800 first commit 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 0000000..14c889f Binary files /dev/null and b/data/NotoSansBold56.vlw differ diff --git a/data/NotoSansSCM14.vlw b/data/NotoSansSCM14.vlw new file mode 100644 index 0000000..0ce9369 Binary files /dev/null and b/data/NotoSansSCM14.vlw differ 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]; + + $("