How were we creating the chatbot

Introduction

In the beginning, we just wanted to create a chatbot that would reliably answer questions based on our resources within our project nestforms.com.

In the ideal world we wanted to have the resources section at the bottom of each question that would lead to relevant resources (like help pages links).

Our data resources are connected from different types of the content: FAQ, Help pages, frequent email responses, template forms, case studies, other blog pages. Obviously all in different formats. Originally cca 10MB of text.

We had this in our mind for some time and in September 2024, we just agreed to go ahead. We are developers, so we did not want just to order some service, we wanted to test the options ourselves.

We did not know enough about what are the options, we just knew there are different available models within ChatGPT (which we were using already) and some options with Google Claude (no experiences) and also there are some options with Amazon AWS which we are using as the cloud platform (but had no experiences with AI services available).

Starting

We have started with ChatGPT as it was the most obvious option. We had a paid plan for $20 and we were able to create a model. We have prepared our data into a JSON file and uploaded it into the model with some textual description of the data in the JSON.

Great news – ChatGPT was able to answer some of the questions. 

It was quite hard to get the resources section as chatGPT was quite unreliable and sometimes hallucinated the URLs. But this was working quite well within the online environment.

Surprise number 1: There is no way to connect to the ChatGPT custom model.

Unfortunately, we could not figure out a way to connect ChatGPT to our custom model via API. This appeared after hours and hours of searching the internet and discussing with ChatGPT.

So we decided to try with openAI which is the mother company of ChatGPT. They have an API that you can connect to.

Surprise number 2: OpenAI custom model require different data.

Unfortunately, they needed the data in a different format. They wanted everything as JSONL (different to chatGPT) and also, they required a rigid structure as question and answer.

So we had to completely review our data to get the question / answer format from our resources.

Surprise number 3: OpenAI custom model does not work the same way as ChatGPT Custom Model.

When we delivered the data, the model started to learn. It was a different process than originally with ChatGPT. This took much longer as this time, the model really trained the neural network – called fine-tuning. 

After it finished the training process, we tested the responses and we were shocked with the responses. The model was answering in a completely different way, linking to our concurrency projects. And there was no chance to get the resources section at the end of the result.

When we dig into why this is the case, the difference is that the Custom model in ChatGPT is searching within our resources and finds the most suitable texts and then tries to create a response from it. But the OpenAI custom model merged the knowledge base into the model and is not able to source any data directly at all.

We tried again to search for better options, if we have made any mistake in our setup, but we could not improve it in any real way.

So we had to resign on working with openAI and ChatGPT as it is simply not offering the way we need.

Moving to Amazon AWS

As mentioned earlier, our projects are already hosted on amazon, so we decided to try this platform.

While the ChatGPT was very clear and straightforward, the Amazon AWS is a complete opposite. The reason for this is that Amazon AWS is providing just the infrastructure, but is not user friendly at all. There are many and many options that you do not understand in the beginning and you are just picking “something”. 

Playing wih Amazon Bedrock.

After some searching, we started creating a Bedrock knowledgebase. Prepared the resources in the fixed JSON structure (different to chatGPT and open AI again). Uploaded the data to S3 and tried to connect. 

As we dug for more and more details, we found out that our option should be to create a vector store database based on our data.

Surprise number 4: We cannot use the JSON model.

Within ChatGPT, the setup was quite intuitive and you were able to describe the structure of the JSON file in the text way, but we could not manage this within Bedrock Knowledgebase. So after many tries, we resigned to JSON format and used csv format.

And now, we can finally see that the knowledgebase is answering with some data from our resources.

Unfortunately we are not able to get any links to our resources now because the knowledgebase is not able to recognise the data from different columns correctly. So digging again, there is an option to fix this, you can create a .metadata.json file that allows you to define what type of data is in which column. It allowed us to specify the column for the link resources. And hurray, we were able to get some resources links now.

We tested some communication within the API with ChatGPT already. And it was quite straight forward. But with Amazon, we reached the issue again. As there are many options, the documentation is also more complicated and nothing is as straight forward. Part of the issues here are the complicated permissions. So it took much longer again. 

Good news – in the end, we were able to connect our amazon.

Surprise number 5: No streaming with Bedrock Flow, Agents or Prompt Management

Now we want to polish it and retrieve data in the ideal format with our resources links.

We started to play with Amazon Bedrock Flow, Amazon Bedrock Agents and Amazon Bedrock Prompt Management. They are all tools that are loosely connected to the knowledgebase. But any time we reached any valuable step forward from the result point of view, the service was not able to stream the response. So we had to wait for the whole response and display the whole response. This took a very long time and completely missed the nice effect of chatbot writing the response. So we always reverted back to the plain knowledgebase.

After many and many tests, we found out that we could use the feature RetrieveAndGenerateStreamCommand which is a special command from the Amazon Bedrock package for the NodeJs. That allowed us to stream the response and also receive other ultimate information required for our resources links section defined in the metadata file. Unfortunately, it does not stream the list items correctly, so when the AI is suggesting the bulletpoint list, the user must wait until the whole list is completed. But we could not set this up in any better way.

So we are now receiving the stream of the content from Knowledgebase and also receiving additional metadata which lets us generate the resources links section manually and place it at the end of the response.

Surprise number 6: Amazon pricing

We are a small service with less than thousand question requests per month, so we need to review the pricing.

When this was set up, we had to enable several services with many settings. It showed that amazon enabled 10 instances for search (and charging for these). So we have found the settings and limited this to 2 instances (which is minimum).

Additional price optimisation

Now the service is running as expected and amazon is charging us about $400 per month. As this is just an additional service and it is not crucial, we were wondering if this can be even cheaper. So after a lot of digging, we have found out that it might be possible to replace the OpenSearch Service with some RDS that might be possible to be paused.

So we have created a RDS cluster for our knowledgebase and set this cluster to be stopped after 15 minutes of inactivity. This will allow the database to stop overnight and then start again when somebody really requests to talk to our chatbot.

We had updated the service, how we are connecting to the knowledgebase, but it works quite well. The user will need to wait several more seconds, but when you handle this reasonably in the interface, it is not too dramatic.

Now we are under $50 per month.

Conclusion

We finally have our chatbot that is doing exactly what we want, supporting only answers from our resources and also supporting a reliable list of the resources links.

If you want to discuss any of the steps above with us or you are interested in deeper knowledge. Contact us and we will agree to some consultancy.
We finished this in March 2025. Since then, we have applied the same process into one of our client in-el.cz website for internal users only.

Technical background

The website is in PHP. PHP does not allow data streaming. So we have also created a NodeJS service that is connected to the website via websocket. This NodeJS is then communicating with Amazon AWS knowledgebase. We have also applied the rate limits (so that the service cannot be overused). And there is also a connection from the NodeJS to PHP to identify the type of the user (subscribed users have different rate limits) and NodeJS can also store the results into PHP for administration purposes.

In the amazon Knowledgebase, we are using Embeddings model: Titan Text Embeddingsv2 and for generating texts claude-3-5-sonnet.

PHP Unset ArrayObject in foreach skip the following item – unexpected behaviour

When you unset the item from array in foreach, all works as expected

$array = [1,2,3,4];
foreach($array as $key => $value) {
  if($value === 2) {
    unset($array[$key]);
    continue;
  }
  echo "$value ";
}

Will return following:

1 3 4

When you want to do the same with ArrayObject, you would expect exactly the same behaviour. Unfortunately, it is not the case.

class MainList extends \ArrayObject {}
$arrayListObject = new MainList([1,2,3,4]);
foreach($arrayListObject as $key => $value) {
  if($value === 2) {
    unset($arrayListObject[$key]);
    continue;
  }
  echo "$value ";
}

Will return following:

1 4

See that the number 3 is missing in the response!!!
The reason for this is that the ArrayObject is behaving like object (not as an array) when looping through foreach.

This could be very confusing when you expect that that the foreach will process all the items.

The best option is to process it via

class MainList extends \ArrayObject {}
$arrayListObject = new MainList([1,2,3,4]);
foreach($arrayListObject->getArrayCopy() as $key => $value) {
  if($value === 2) {
    unset($arrayListObject[$key]);
    continue;
  }
  echo "$value ";
}

Will return following (as expected):

1 3 4

Updated script for downloading file from javascript

I had to use the ajax to download a file internally on our NestForms project. I was searching for a solution and found one on Filamentgroup website.

Unforutunatelly, there were some issues that it did not correctly encoded the request. So I had to update the script in order to make it working. See the code below. I hope that it will help you solve your problems.

jQuery.download = function(url, data, method){
  //url and data options required
  if( url && data ){
    //data can be string of parameters or array/object
    data = typeof data == 'string' ? data : jQuery.param(data);
    //split params into form inputs
    var inputs = new Array();
    jQuery.each(data.split('&'), function(){
      var pair = this.split('=');
      inputs[inputs.length] = $('<input name="'+ decodeURIComponent(pair[0]) +'" type="hidden" />')
        .val(decodeURIComponent( pair[1].replace(/\+/g, " ")));
    });
    //send request
    jQuery('<form action="'+ url +'" method="'+ (method||'post') +'">'+'</form>').append(inputs)
    .appendTo('body').submit().remove();
  };
};

OnBeforeUnload fun

Yesterday, we have encountered issues regarding window.onbeforeunload function. We have implemented checking of json transport issues, but this throws errors, when the page is beeing unloaded and we needed to hide these errors.

So we implemented onBeforeUnload function, however we needed to preserve any previous onBeforeUnload functions. The issue here is that IE does not understand, that return value NULL is supposed to mean ‘Do not show the confirm’.

var origOnBeforeUnload = window.onbeforeunload;
window.onbeforeunload = function(event) {
  if(typeof origOnBeforeUnload === 'function') {
    return origOnBeforeUnload(event);
  }
}

We needed to implement little more robust checking, and when we do not want to show a message, you need to not return anything.

var origOnBeforeUnload = window.onbeforeunload;
window.onbeforeunload = function(event) {
  //do any code you want to
  if(typeof origOnBeforeUnload === 'function') {
    var out = origOnBeforeUnload(event);
    if(typeof out !== 'undefined' && out !== null && out !== false)
      return out;
    }
  }
}

jQuery simple-color-picker

We use simpleColorPicker jQuery plugin.

We had an issue that function .live() is depricated in jQuery 1.9.

We solved it by replacing following line:

Line 56: $(‘body’).live(‘click’, function() {

Correction: $(document).on(‘click’, ‘body’, function() {

PHP – Post name max length

Hi everyone,
I have found problem with PHP Version 5.3.2-1ubuntu4.18. When I tried to submit a form, values appeared in $HTTP_RAW_POST but there was nothing in $_POST. Problem was in legnth of name attribut. In other PHP versions is this length virtually unlimited, but in this version is length of each array key limited on 64 characters. Hope that this information will help someone 🙂


input type="text" id="text1" name="this_is_very_looooooooooooooooooooooooooooooooooooooooooong_name_which_will_not_submit_the_input" value="text"

Roman

Android – showing Google map in WebView

Hi everyone,

I have resolved my little problem with showing Google map via WebView. When I tried to show map in WebView, I had seen only white box and nothing more, after short delay loading animation appeared and it was all. I was finding solution, but nowhere was described what I am doing wrong. Solution was very simple – I had to set myWebView.getSettings().setJavaScriptEnabled(true); … Simple, isn’t it? 🙂 I hope that it will be helpful for someone who will has similar stupid problem. 🙂

Android – Drawing route on MapView

Hi everyone,

I had quite big problem with drawing route on MapView. Tutorials on the web which I found are quite confusing or with bugs. So I took several of them and did own program, which using some code from several tutorials . Here is source of my map activity:

package tvarwebu.projects.map;

import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.List;

import com.google.android.maps.GeoPoint;
import com.google.android.maps.MapActivity;
import com.google.android.maps.MapController;
import com.google.android.maps.MapView;
import com.google.android.maps.Overlay;
import com.google.android.maps.OverlayItem;

import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.drawable.Drawable;
import android.location.Location;
import android.location.LocationManager;
import android.os.Bundle;
import android.os.Handler;

public class MapProjectActivity extends MapActivity {

GeoPoint myPoint = null;
GeoPoint nextPoint = null;
MapView map;
private Road mRoad;
int latSpan = -1;
boolean moving = false;
long lastTimestamp = 0;
int pointCount = 0;
int lat = 0;
int lng = 0;
Location currLoc = null;
OverlayItem currPoint;
MyItemizedOverlay currItemizedOverlay;
List<Overlay> mapOverlays;
LocationManager lm;

@Override
protected void onCreate(Bundle savedInstanceState) {

  super.onCreate(savedInstanceState);
  setContentView(R.layout.main);

  map = (MapView) findViewById(R.id.mapView);
  map.setBuiltInZoomControls(true);
  map.setStreetView(true);
  map.setSatellite(false);

  // Your position initialize
  lat = 53324388;
  lng = -6263194;

  myPoint = new GeoPoint(lat, lng);

  Drawable drawable = this.getResources().getDrawable(
  R.drawable.flag_green);

  MyItemizedOverlay itemizedoverlay = new MyItemizedOverlay(drawable, this);

  OverlayItem overlayitem = new OverlayItem(myPoint, "Start Position", "Your start position");

  itemizedoverlay.addOverlay(overlayitem);

  // Position of next point
  int lat2 = 53348084;
  int lng2 = -6292434;

  nextPoint = new GeoPoint(lat2, lng2);

  OverlayItem overlayitem2 = new OverlayItem(nextPoint, "Position", "Position of next point");

  itemizedoverlay.addOverlay(overlayitem2);

  mapOverlays = map.getOverlays();
  mapOverlays.add(itemizedoverlay);

  Drawable draw2 = this.getResources().getDrawable(R.drawable.flag_red);

  MyItemizedOverlay over2 = new MyItemizedOverlay(draw2, this);
  over2.addOverlay(overlayitem2);

  mapOverlays.add(over2);

  // zooming to both points
  int maxLatitude = Math.max(lat, lat2);
  int minLatitude = Math.min(lat, lat2);
  int maxLongitude = Math.max(lng, lng2);
  int minLongitude = Math.min(lng, lng2);

  MapController mc = map.getController();
  mc.zoomToSpan(maxLatitude - minLatitude, maxLongitude - minLongitude);
  mc.animateTo(new GeoPoint((maxLatitude + minLatitude) / 2, (maxLongitude + minLongitude) / 2));

  map.invalidate();

  //Drawing path in new Thread
  new Thread() {

  @Override
  public void run() {
    double fromLat = Double.valueOf(myPoint.getLatitudeE6()) / 1000000.0,
    fromLon = Double.valueOf(myPoint.getLongitudeE6()) / 1000000.0;
    double toLat = Double.valueOf(nextPoint.getLatitudeE6()) / 1000000.0,
    toLon = Double.valueOf(nextPoint.getLongitudeE6()) / 1000000.0;

    String url = RoadProvider.getUrl(fromLat, fromLon, toLat, toLon);
    InputStream is = getConnection(url);
    mRoad = RoadProvider.getRoute(is);
    mHandler.sendEmptyMessage(0);
  }
}.start();

}

Handler mHandler = new Handler() {
  public void handleMessage(android.os.Message msg) {
    MapOverlay mapOverlay = new MapOverlay(mRoad, map);
    List<Overlay> listOfOverlays = map.getOverlays();
    // listOfOverlays.clear();
    listOfOverlays.add(mapOverlay);
    map.invalidate();
  };
};

private InputStream getConnection(String url) {
  InputStream is = null;
  try {
    URLConnection conn = new URL(url).openConnection();
    is = conn.getInputStream();
  } catch (MalformedURLException e) {
    e.printStackTrace();
  } catch (IOException e) {
    e.printStackTrace();
  }
  return is;
}

@Override
protected boolean isRouteDisplayed() {
  // TODO Auto-generated method stub
  return false;
}
// Map extends overlay for drawing path
class MapOverlay extends com.google.android.maps.Overlay {
  Road mRoad;
  ArrayList<GeoPoint> mPoints;

  public MapOverlay(Road road, MapView mv) {
    mRoad = road;
    // mRoute is field of route points getting from Google Maps
    if (road.mRoute.length > 0) {
      mPoints = new ArrayList<GeoPoint>();
      for (int i = 0; i < road.mRoute.length; i++) {
        mPoints.add(new GeoPoint(
        (int) (road.mRoute[i][1] * 1000000),
        (int) (road.mRoute[i][0] * 1000000)));
      }
    }
  }

  @Override
  public void draw(Canvas canvas, MapView mv, boolean shadow) {
    drawPath(mv, canvas);
  }

  public void drawPath(MapView mv, Canvas canvas) {
    int x1 = -1, y1 = -1, x2 = -1, y2 = -1;
    Paint paint = new Paint();
    latSpan = mv.getLatitudeSpan();
    paint.setColor(Color.parseColor("#998447cc"));
    paint.setStyle(Paint.Style.STROKE);
    paint.setStrokeWidth(6);
    if (mPoints != null) {
      for (int i = 0; i < mPoints.size(); i++) {
        Point point = new Point();
        mv.getProjection().toPixels(mPoints.get(i), point);
        x2 = point.x;
        y2 = point.y;
        if (i > 0) {
          canvas.drawLine(x1, y1, x2, y2, paint);
        }
        x1 = x2;
        y1 = y2;
      }
    }
  }
}
}

In addition, you will need this files:

1. Road.java

package tvarwebu.projects.map;

public class Road {
public String mName;
public String mDescription;
public int mColor;
public int mWidth;
public double[][] mRoute = new double[][] {};
public Point[] mPoints = new Point[] {};

}

2. Point.java

package tvarwebu.projects.map;

public class Point {
  String mName;
  String mDescription;
  String mIconUrl;
  double mLatitude;
  double mLongitude;
}

3. RoadProvider.java

package tvarwebu.projects.map;

import java.io.IOException;
import java.io.InputStream;
import java.util.Stack;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

public class RoadProvider {

public static Road getRoute(InputStream is) {
  KMLHandler handler = new KMLHandler();
  try {
    SAXParser parser = SAXParserFactory.newInstance().newSAXParser();
    parser.parse(is, handler);
  } catch (ParserConfigurationException e) {
    e.printStackTrace();
  } catch (SAXException e) {
    e.printStackTrace();
  } catch (IOException e) {
    e.printStackTrace();
  }
  return handler.mRoad;
}

public static String getUrl(double fromLat, double fromLon, double toLat, double toLon) {// connect to map web service
  StringBuffer urlString = new StringBuffer();
  urlString.append("http://maps.google.com/maps?f=d&hl=en");
  urlString.append("&saddr=");// from
  urlString.append(Double.toString(fromLat));
  urlString.append(",");
  urlString.append(Double.toString(fromLon));
  urlString.append("&daddr=");// to
  urlString.append(Double.toString(toLat));
  urlString.append(",");
  urlString.append(Double.toString(toLon));
  urlString.append("&ie=UTF8&0&om=0&output=kml");

  String ggg = urlString.toString();
  return urlString.toString();
}
}

class KMLHandler extends DefaultHandler {
  Road mRoad;
  boolean isPlacemark;
  boolean isRoute;
  boolean isItemIcon;
  private Stack mCurrentElement = new Stack();
  private String mString;

  public KMLHandler() {
  mRoad = new Road();
}

public void startElement(String uri, String localName, String name, Attributes attributes) throws SAXException {
  mCurrentElement.push(localName);
  if (localName.equalsIgnoreCase("Placemark")) {
    isPlacemark = true;
    mRoad.mPoints = addPoint(mRoad.mPoints);
  } else if (localName.equalsIgnoreCase("ItemIcon")) {
    if (isPlacemark)
      isItemIcon = true;
    }
    mString = new String();
  }

  public void characters(char[] ch, int start, int length) throws SAXException {
    String chars = new String(ch, start, length).trim();
    mString = mString.concat(chars);
  }

  public void endElement(String uri, String localName, String name) throws SAXException {
    if (mString.length() > 0) {
      if (localName.equalsIgnoreCase("name")) {
        if (isPlacemark) {
          isRoute = mString.equalsIgnoreCase("Route");
          if (!isRoute) {
            mRoad.mPoints[mRoad.mPoints.length - 1].mName = mString;
          }
        } else {
          mRoad.mName = mString;
        }
      } else if (localName.equalsIgnoreCase("color") && !isPlacemark) {
        mRoad.mColor = Integer.parseInt(mString, 16);
      } else if (localName.equalsIgnoreCase("width") && !isPlacemark) {
        mRoad.mWidth = Integer.parseInt(mString);
      } else if (localName.equalsIgnoreCase("description")) {
        if (isPlacemark) {
          String description = cleanup(mString);
          if (!isRoute)
            mRoad.mPoints[mRoad.mPoints.length - 1].mDescription = description;
          else
            mRoad.mDescription = description;
        }
      } else if (localName.equalsIgnoreCase("href")) {
        if (isItemIcon) {
          mRoad.mPoints[mRoad.mPoints.length - 1].mIconUrl = mString;
        }
      } else if (localName.equalsIgnoreCase("coordinates")) {
        if (isPlacemark) {
          if (!isRoute) {
            String[] xyParsed = split(mString, ",");
            double lon = Double.parseDouble(xyParsed[0]);
            double lat = Double.parseDouble(xyParsed[1]);
            mRoad.mPoints[mRoad.mPoints.length - 1].mLatitude = lat;
            mRoad.mPoints[mRoad.mPoints.length - 1].mLongitude = lon;
          } else {
            String[] coodrinatesParsed = split(mString, " ");
            int count = 0;
            if(mRoad.mRoute.length < 2)
              mRoad.mRoute = new double[coodrinatesParsed.length][2];
            else {
              double[][] mRouteTmp = mRoad.mRoute;
              mRoad.mRoute = new double[mRouteTmp.length + coodrinatesParsed.length][2];
              //for (int i = 0; i < mRouteTmp.length; i++) {
              //mRoad.mRoute[i] = mRouteTmp[i];
              //mRoad.mRoute[i][i] = mRouteTmp[i][i];
              //}
              System.arraycopy(mRouteTmp, 0, mRoad.mRoute, 0, mRouteTmp.length);

              count = mRouteTmp.length;
            }

            for (int i = count; i < (mRoad.mRoute.length); i++) {
              String[] xyParsed = split(coodrinatesParsed[i - count], ",");
              for (int j = 0; j < 2 && j < xyParsed.length; j++)
                mRoad.mRoute[i][j] = Double.parseDouble(xyParsed[j]);
            }
          }
        }
      }
    }
    mCurrentElement.pop();
    if (localName.equalsIgnoreCase("Placemark")) {
      isPlacemark = false;
      if (isRoute)
      isRoute = false;
    } else if (localName.equalsIgnoreCase("ItemIcon")) {
      if (isItemIcon)
        isItemIcon = false;
    }
  }

  private String cleanup(String value) {
    String remove = "<br/>";
    int index = value.indexOf(remove);
    if (index != -1)
    value = value.substring(0, index);
    remove = " ";
    index = value.indexOf(remove);
    int len = remove.length();
    while (index != -1) {
      value = value.substring(0, index).concat(
      value.substring(index + len, value.length()));
      index = value.indexOf(remove);
    }
    return value;
  }

  public Point[] addPoint(Point[] mPoints) {
    Point[] result = new Point[mPoints.length + 1];
    for (int i = 0; i < mPoints.length; i++)
      result[i] = mPoints[i];
    result[mPoints.length] = new Point();
    return result;
  }

  private static String[] split(String strString, String strDelimiter) {
    String[] strArray;
    int iOccurrences = 0;
    int iIndexOfInnerString = 0;
    int iIndexOfDelimiter = 0;
    int iCounter = 0;
    if (strString == null) {
      throw new IllegalArgumentException("Input string cannot be null.");
    }
    if (strDelimiter.length() <= 0 || strDelimiter == null) {
      throw new IllegalArgumentException("Delimeter cannot be null or empty.");
    }
    if (strString.startsWith(strDelimiter)) {
      strString = strString.substring(strDelimiter.length());
    }
    if (!strString.endsWith(strDelimiter)) {
      strString += strDelimiter;
    }
    while ((iIndexOfDelimiter = strString.indexOf(strDelimiter,
      iIndexOfInnerString)) != -1) {
      iOccurrences += 1;
      iIndexOfInnerString = iIndexOfDelimiter + strDelimiter.length();
    }

    strArray = new String[iOccurrences];
    iIndexOfInnerString = 0;
    iIndexOfDelimiter = 0;
    while ((iIndexOfDelimiter = strString.indexOf(strDelimiter,
      iIndexOfInnerString)) != -1) {
      strArray[iCounter] = strString.substring(iIndexOfInnerString,
      iIndexOfDelimiter);
      iIndexOfInnerString = iIndexOfDelimiter + strDelimiter.length();
      iCounter += 1;
    }

    return strArray;
  }
}

4. MyItemizedOverlay.java :

package tvarwebu.projects.map;

import java.util.ArrayList;
import android.app.AlertDialog;
import android.content.Context;
import android.graphics.drawable.Drawable;
import com.google.android.maps.ItemizedOverlay;
import com.google.android.maps.OverlayItem;

public class MyItemizedOverlay extends ItemizedOverlay<OverlayItem> {
  private ArrayList<OverlayItem> mOverlays = new ArrayList<OverlayItem>();
  private Context mContext;

  public MyItemizedOverlay(Drawable defaultMarker, Context context) {
    super(boundCenterBottom(defaultMarker));
    mContext = context;
  }

  public void addOverlay(OverlayItem overlay) {
    mOverlays.add(overlay);
    populate();
  }

  @Override
  protected OverlayItem createItem(int i) {
    return mOverlays.get(i);
  }

  @Override
  public int size() {
    return mOverlays.size();
  }

  public void removeLast(){
    mOverlays.remove(mOverlays.size()-1);
  }

  @Override
  protected boolean onTap(int index) {
    OverlayItem item = mOverlays.get(index);
    AlertDialog.Builder dialog = new AlertDialog.Builder(mContext);
    dialog.setTitle(item.getTitle());
    dialog.setMessage(item.getSnippet());
    dialog.show();
    return true;
  }
}

When you change names of package, you should get this screen after launch:

Hope that this article helps you with this problem 🙂 See the comple source code of Drawing route on MapView.

by Roman Holomek,  TvářWebu