Tomtom with Firebase #2: FireMarker CRUD. Fix exception to load class

Tomtom with Firebase #2: FireMarker CRUD. Fix exception to load class

Previous Part: Tomtom SDK with Firebase: Email+Google SignIn+Facebook SignIn Setup.

This is article is working on Tomtom SDK 2.045 and Firebase-UI 3.3.0 and Firebase 12.0.1 . You need to make change(click the link) before you continue.

If you get in touch database, you will know what is CRUD: Create, Read, Update and Delete. In this part, let's create some markers from three different accounts. Firebase realtime database is a NoSQL database. So you don't need to create table first before you insert anything. You can insert any JSON friendly data in the any types of table. Before we work in the code, the realtime database is locked.

We need to unlock it in the rule.


auth != null, which means authenticated user only.



Now, backup your code into your Version Control such as Github. You must do it before you make any big change. So we can feel free to mess around.

In the map, the marker contains balloon image, balloon text and tag. I mixed text and tag before this part. Now, I need the tag to hold the firebase key to update and delete the marker. So let's make change in addMarker() to separate the text and the tag. I sure that we will have more functions in the future. I will move addMarker() into a new class, MapIO.java in utils package. To separate with TomtomMap.addMarker(), let's call it addMyMarker().

public class MapIO {
    /* Log tag and shortcut */  
    final static String TAG = "MYLOG MapIO";
    public static void ltag(String message) { Log.i(TAG, message); }

    private Context context;

    public MapIO(Context context) {
        this.context = context;
    }

    /*
        Marker Functions
     */  
    public Marker addMyMarker(TomtomMap tomtomMap, LatLng position, String text, String tag, String mImageUrl) {
        MarkerBuilder markerBuilder;

        HomanMarkerLayoutBalloon mBalloon = new HomanMarkerLayoutBalloon(
                R.layout.balloon_layout,
                context,
                text,
                mImageUrl);

        markerBuilder = new MarkerBuilder(position)
                .icon(Icon.Factory.fromResources(context, R.drawable.ic_favourites))
                .tag(tag)
                .markerBalloon(mBalloon)
                .iconAnchor(MarkerAnchor.Bottom)
                .decal(true);

        Marker mMarker = tomtomMap.addMarker(markerBuilder);
        mBalloon.getBalloonOffset(mMarker);

        return mMarker;
    }
}

The HomanMarkerLayoutBallon has to add the imageUrl because we don't want database to handle the image data. For big files, Firestore will be the best suit for the work.

public class HomanMarkerLayoutBalloon extends BaseMarkerBalloonLayout {  
    /* Log tag and shortcut */  
    final static String TAG = "MYLOG Balloon";  
    public static void ltag(String message) { Log.i(TAG, message); }  
  
    private ImageView imageView;  
    private String text;  
    private String url;  
    private Context context;  
  
    public HomanMarkerLayoutBalloon(int i, Context context, String text, String url) {  
        super(i);  
        this.context = context;  
        this.text = text;  
        this.url = url;  
    }  
  
    public String getUrl() { return url; }  
  
    @Override  
    public void onBindView(View view, Marker marker) {  
        imageView = (ImageView) view.findViewById(R.id.balloonImageView);  
        TextView textView = (TextView) view.findViewById(R.id.balloonTextView);  
  
        if (url != null && url != "") {  
  
            ltag("url: "+url);  
            //update balloon image  
            File f = new File(url);  
            Target mTarget = new Target() {  
                @Override  
                public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) {  
                    imageView.setImageBitmap(bitmap);  
                }  
  
                @Override  
                public void onBitmapFailed(Drawable errorDrawable) { ltag("Read File Error!"); }  
  
                @Override  
                public void onPrepareLoad(Drawable placeHolderDrawable) { }  
            };  
            Picasso.with(context)  
                    .load(f)  
                    .resize(80, 50)  
                    .centerInside()  
                    .into(mTarget);  
            imageView.setTag(mTarget);  
        }  
  
        textView.setText(text);  
    }  
  
    public void setText(String text) { this.text = text; }  
  
    public String getText() { return text; }  
  
    @NonNull  
    @Override  
    public Point getBalloonOffset(Marker marker) {...}  
  
}  

If you want to save everything in one line, we need to a class to help us the input the marker. Let's call FireMarker.java. It contains similar arguments like addMyMarker().

After you input the variables, Alt+Insert for constructor and Alt+Insert for getter and setter.




In MainActivity, find all addMarker and change them all.

MapIO mapIO = new MapIO(MainActivity.this);  
  
//map event: long click  
TomtomMapCallback.OnMapLongClickListener onMapLongClickListener =  
		new TomtomMapCallback.OnMapLongClickListener() {  
			@Override  
			public void onMapLongClick(LatLng latLng) {  
				mMarker = mapIO.addMyMarker(tomtomMap, latLng, "?no", "", "");  
				fireMarker = new FireMarker(latLng, "?no", "", "");  
			}  
		};  
...

public void modifyText(View v) {
	//hide and stop timer
	hideMarkerButtons(Choice.ALL);

	//get balloon image
	HomanMarkerLayoutBalloon mBalloon = (HomanMarkerLayoutBalloon) mMarker.getMarkerBalloon().get();
	String mImageUrl = mBalloon.getUrl();
	String mText  = mBalloon.getText();
	urlToBitmap(mImageUrl);

	//show Eidttext box
	tagConstraintLayout.setVisibility(View.VISIBLE);
	if (mText.equals("?no") || mText == null) {
		tagEditText.setText("");
	} else {
		tagEditText.setText(mText);
	}
}
...

public void updateText(View v) {  
	String text = tagEditText.getText().toString();

	ltag("Update Text: "+text);

	//the Tomtom Marker doesn't have setTag(), setId()
	//So I have to delete one and create a new one
	LatLng x = mMarker.getPosition();
	String tag = mMarker.getTag().toString();

	tomtomMap.removeMarker(mMarker);
	mMarker = mapIO.addMyMarker(tomtomMap, x, text, tag, imageUrl);
	fireMarker = new FireMarker(x, text, tag, imageUrl);

	ltag("New marker's tag: "+mMarker.getTag().toString());

	//Clear Text box
	tagEditText.setText("");
	//hide Text box
	tagConstraintLayout.setVisibility(View.INVISIBLE);
	//hide keyboard
	hideKeyboard(getApplicationContext(), tagEditText);
	resetDefaultMarker();
}

In the layout, I add a new button to save marker in map_marker_button.xml.

Save button call saveMarker(), which automatically check the authentication before the marker data transfer to database.

/*  
    Save and update Marker to Firebase Database
 */  
boolean boolSaveMarker = false;
public void saveMarker(View v) {
    ltag("Save Marker");

    if (user != null) {
        saveToDatabase();
    } else {
        ltag("Sign in user.");

        //ask user to sign in
        signInFirebase();
        boolSaveMarker = true;
    }
}

After user signed in, the callback will return to onActivityResult().

@SuppressLint("RestrictedApi")  
@Override  
protected void onActivityResult(int requestCode, int resultCode, Intent data) {  
	...  
	switch (requestCode) {  
		case REQUEST_IMAGE_BALLOON: ...  
  
		case RC_SIGN_IN:  
  
			IdpResponse response = IdpResponse.fromResultIntent(data);  
  
			ltag("Activity Result:");  
  
			if (resultCode == RESULT_OK) {  
				...  
  
				if (boolSaveMarker) saveToDatabase();  
  
			} else {...}  
			break;  
	}  
}

Next, let's add create and update function to database. Create and update are twin brothers. Let's put them in the same function, called saveToDatabase().

Database path:
    tomtom-test\
        + markers\
            + user1 id\
                + marker1, + marker2, ...
            + user2 id\
                + marker1, + marker2, ...

Here is the path in the database. It'll give you the clear idea how you write the codes. On firebase website, it teaches you use Map to insert the data. Actually, you do it at once.

private void saveToDatabase() {  
	ltag("user id: "+user.getUid().toString());  
	ltag("fireMarker: "+fireMarker.toString());  

	String uid = user.getUid().toString();  

	DatabaseReference markerRef = tomtomRef.child("markers");  
	DatabaseReference userRef =  markerRef.child(uid);  

	if (fireMarker.getTag() == null || fireMarker.getTag() == "") {  

		ltag("Add new marker");  

		//get a new key  
		DatabaseReference myRef = userRef.push();  
		String key = myRef.getKey();  

		fireMarker.setTag(key);  
		ltag("new key: "+key+", fireMarker.tag: "+fireMarker.getTag());  

		myRef.setValue(fireMarker, new DatabaseReference.CompletionListener() {  
			@Override  
			public void onComplete(DatabaseError databaseError, DatabaseReference databaseReference) {  

				if (databaseError == null) {  
					ltag("Add successful to database.");  

					imageUrl = "";  
					boolSaveMarker = false;  
				} else {  
					ltag("Save failed: " + databaseError.toString());  
				}  
			}  
		}); 

In the create section, you can push first to get the unique key and set tag = key. So you can update or delete in high speed without search. The setValue() can read public data of FireMarker automatically and insert them into database. Next half is update.

	} else {  
		ltag("Update marker.");  

		String key = fireMarker.getTag();  

		Map<String, Object> childUpdates = new HashMap<>();  
		childUpdates.put("/markers/"+uid+"/"+ key, fireMarker);  

		tomtomRef.updateChildren(childUpdates, new DatabaseReference.CompletionListener() {  
			@Override  
			public void onComplete(DatabaseError databaseError, DatabaseReference databaseReference) {  

				if (databaseError == null) {  
					ltag("Update successful to database.");  


					imageUrl = "";  
					boolSaveMarker = false;  
				} else {  
					ltag("Update failed: " + databaseError.toString());  
				}  
			}  
		});  
	}  
}

You see that I use Mapping in this half. Once you set the path correctly, it is easy to update. Your tag is your shortcut.

Now, test it. Run!

Create Function has no bug. Let's update the marker. The test result is fail. The tag of marker is still blank. So the database inserts a new line instead of update. Can we update the tag of marker? In this version of Tomtom SDK, you can't set tag for marker. There is a way around that: delete it and insert a new marker. Let's create a new function called, renewMarker().

private void renewMarker(LatLng x, String text, String tag, String url) {
    tomtomMap.removeMarker(mMarker);
    mMarker = mapIO.addMyMarker(tomtomMap, x, text, tag, url);
    fireMarker = new FireMarker(x, text, tag, url);
}

Insert renewMarker() in updateText() and saveToDatabase().

public void updateText(View v) {
    String text = tagEditText.getText().toString();

    ltag("Update Text: "+text);

    LatLng x = mMarker.getPosition();
    String tag = mMarker.getTag().toString();

    renewMarker(x, text, tag, imageUrl);

    ...
}
...
private void saveToDatabase() {
    ...

    if (fireMarker.getTag() == null || fireMarker.getTag().equals("")) {
        ltag("Add new marker");
        ...
        myRef.setValue(fireMarker, new DatabaseReference.CompletionListener() {
            @Override   
            public void onComplete(DatabaseError databaseError, DatabaseReference databaseReference) {

                if (databaseError == null) {
                    ltag("Add successful to database.");
                    renewMarker(mMarker.getPosition(),
                            fireMarker.getText(),
                            fireMarker.getTag(),
                            fireMarker.getPhotoUrl());
                    ...
                } else {
                    ltag("Save failed: " + databaseError.toString());
                }
            }
        });

    } else {
        ltag("Update marker.");
        ...
        tomtomRef.updateChildren(childUpdates, new DatabaseReference.CompletionListener() {
            @Override   
            public void onComplete(DatabaseError databaseError, DatabaseReference databaseReference) {

                if (databaseError == null) {
                    ltag("Update successful to database.");
                    renewMarker(mMarker.getPosition(),
                            fireMarker.getText(),
                            fireMarker.getTag(),
                            fireMarker.getPhotoUrl());
                    ...
                } else {
                    ltag("Update failed: " + databaseError.toString());
                }
            }
        });
    }
}

This time, the update is working fine because the tag is not blank anymore. Here is the logcat:

MYLOG TOMTOM: Update marker.
MYLOG TOMTOM: Update successful to database.

Next section is Read. Let's call loadFireMarkers(). It is not common function. So we can put it in the right hand menu, main_menu.xml.

<itemandroid:id="@+id/firebaseRec"android:title="Firebase Record" ><menu ><group ><itemandroid:id="@+id/firebaseLogin"android:title="Firebase Login" /><itemandroid:id="@+id/loadMarkers"android:title="Load Markers" /></group></menu>
</item>

And add the new function into the onOptionItemSelected(). In last part, I have a useless function called firebaseSignIn(), you can remove that.

    case R.id.firebaseLogin:
        signInFirebase();
        return true;

    case R.id.loadMarkers:
        loadFireMarkers();
        return true;

    ...

    default:
        return false;

}

At this step, we need to sign out current user so we can input different markers for different user.

private void signInFirebase() {
    ltag("Sign in request");
    
    //sign out user
    mAuth.signOut();
    
    startActivityForResult(
            AuthUI.getInstance()
                    .createSignInIntentBuilder()
                    .setAvailableProviders(providers)
                    .build(),
            RC_SIGN_IN);
}

In loadFireMarkers(), we need to check sign in status. If the Auth is good, we clear the screen and load markers by user id.

boolean boolLoadMarkers =  false;  
private void loadFireMarkers() {  
	//not signed in  
	if (user == null) {  
		//ask user to sign in  
		signInFirebase();  
		boolLoadMarkers = true;  
	} else {  
		String uid = user.getUid().toString();  
		DatabaseReference markerRef = tomtomRef.child("markers");  
		DatabaseReference userRef =  markerRef.child(uid);  
  
		//clear screen  
		tomtomMap.removeMarkers();  
  
		userRef.limitToFirst(15).addValueEventListener(new ValueEventListener() {  
			@Override  
			public void onDataChange(DataSnapshot dataSnapshot) {  
				long numChildren = dataSnapshot.getChildrenCount();  
				ltag("DB children: " + numChildren);  
  
				for (DataSnapshot child : dataSnapshot.getChildren()) {  
					ltag("id: "+child.getKey());  
  
					FireMarker fireMarker = child.getValue(FireMarker.class);
				}  
			}  
  
			@Override  
			public void onCancelled(DatabaseError databaseError) {  
				Log.w(TAG, "loadPost:onCancelled", databaseError.toException());  
			}  
		});  
	}  
}

Also, we have to hand the callback at onActivityResult().

@SuppressLint("RestrictedApi")  
@Override  
protected void onActivityResult(int requestCode, int resultCode, Intent data) {  
	...

	switch (requestCode) {  
		...
		case RC_SIGN_IN:  
			... 

			if (resultCode == RESULT_OK) {  
				...

				if (boolSaveMarker) saveToDatabase();  
				if (boolLoadMarkers) loadFireMarkers();  

			} else {  
				...  

				boolSaveMarker = false;  
				boolLoadMarkers = false;  

				... 
			}  
			break;  
	}  
}

Let's Run! Error,

com.google.firebase.database.DatabaseException: 
Class com.tomtom.online.sdk.common.location.LatLng does not define a 
no-argument constructor.

So we cannot use LatLng directly, because it doesn't have an empty constructor. That's a very simple error! We can manually map each variable. You can do that. We also can make one of our own class to fix that. (I have tried to extends LatLng. That doesn't work.) Take a look at the database, copy the variables to a new class MyLatLng.java.

Now, we need some function to convert MyLatLng to LatLng or other way around.

private LatLng mLatLngToLatLng (MyLatLng x) {
    return new LatLng(x.getLatitude(), x.getLontitude());
}

private MyLatLng latlngToMyLatLng(LatLng x) {
    return new MyLatLng(x);
}

private Marker firemarkerToMarker(FireMarker fireMarker) {
    return mapIO.addMyMarker(tomtomMap,
            mLatLngToLatLng(fireMarker.getLatLng()),
            fireMarker.getText(),
            fireMarker.getTag(),
            fireMarker.getPhotoUrl());
}

private FireMarker markerToFireMarker(Marker x) {
    HomanMarkerLayoutBalloon mBalloon = 
        (HomanMarkerLayoutBalloon) x.getMarkerBalloon().get();

    return new FireMarker(latlngToMyLatLng(x.getPosition()), mBalloon.getText(),
                        x.getTag().toString(), mBalloon.getUrl());
}

The renewMarker() can remove line fireMarker. We only need fireMarker in saveToDatabase(). Add a new line in the saveToDatabase().

private void saveToDatabase() {

    fireMarker = markerToFireMarker(mMarker);

There's a small change in FireMarker(). Have you seen the empty constructor? That's the solution of about the exception error.

public class FireMarker {

    private MyLatLng latLng;
    ...

    public FireMarker() {  }

    public FireMarker(MyLatLng latLng,...) {
        ...
    }

Now, let's load the markers on the map in loadFireMarkers().

userRef.limitToFirst(15).addValueEventListener(new ValueEventListener() {
    @Override   
    public void onDataChange(DataSnapshot dataSnapshot) {
        ...
        for (DataSnapshot child : dataSnapshot.getChildren()) {
            ...
            FireMarker fMarker = child.getValue(FireMarker.class);
            mMarker = firemarkerToMarker(fMarker);
        }
    }

Now, you need to clear the old data in Firebase before we run again. The old LatLng is not compatible with the MyLatLng. Run!

Great! It's working.

Next, let's delete marker from database. Before we delete a fireMarker, we need ask user whether he/she wants to delete the one online. We can create a new layout called remove_firemarker_layout.xml.

The diaglog box of Android is ugly. If you have time, please create a good looking version instead of the plain text version. Here is mine.

Don't use AlertDialog, or you will have a ugly BLACK background. Please use Dialog instead of AlterDialog. But I suggest that you need to try AlertDialog. Some of you may want a black version.

Next, let's input the code in MainActivity. First, let's extend the function of removeMakrer(). I also add a patch to reset the balloon image to default after the marker removed.

private void resetDefaultMarkerBalloon() {
    balloonImageView.setImageDrawable(getDrawable(R.drawable.balloon));
}

public void removeMarker(View v) {
    ltag("Remove marker id: "+mMarker.getId());
    hideMarkerButtons(Choice.ALL);
    fireMarker = markerToFireMarker(mMarker);
    //remove the marker   
    tomtomMap.removeMarker(mMarker);

    resetDefaultMarkerBalloon();
    if (user != null)
        askToRemoveOnlineMarker();
}

public void clearAllMarkers(View v) {
    ltag("Remove all markers on screen.");
    hideMarkerButtons(Choice.ALL);
    //remove all markers   
    tomtomMap.removeMarkers();

    resetDefaultMarkerBalloon();
    if (user != null)
        askToRemoveOnlineMarker("all");
}

The next part is the dialog box.

private void askToRemoveOnlineMarker(String choice) {  
	Dialog myDialog = new Dialog(this, R.style.CustomDialog);  
  
	View alertView;  
	LayoutInflater inflater = LayoutInflater.from(getApplicationContext());  
	alertView = inflater.inflate(R.layout.remove_firemarker_layout, null);  
  
	TextView yesTV = (TextView) alertView.findViewById(R.id.yesTV);  
  
	TextView removeFiremarkerTitleTV = (TextView) alertView.findViewById(R.id.removeFiremarkerTitleTV);  
	switch (choice) {  
		case "one":  
			removeFiremarkerTitleTV.setText("Remove Online Marker\nToo?");  
			yesTV.setOnClickListener(new View.OnClickListener() {  
				@Override  
				public void onClick(View v) {  
					removeOnlineMarker();  
					myDialog.dismiss();  
				}  
			});  
			break;  
  
		case "all":  
			removeFiremarkerTitleTV.setText("Remove All Online\nMarkers?");  
			yesTV.setOnClickListener(new View.OnClickListener() {  
				@Override  
				public void onClick(View v) {  
					removeAllOnlineMarkers();  
					myDialog.dismiss();  
				}  
			});  
			break;  
  
	}  
  
	TextView noTV = (TextView) alertView.findViewById(R.id.noTV);  
	noTV.setOnClickListener(new View.OnClickListener() {  
		@Override  
		public void onClick(View v) {  
			myDialog.dismiss();  
		}  
	});  
  
	myDialog.setContentView(alertView);  
	myDialog.getWindow().setBackgroundDrawable(new ColorDrawable(android.graphics.Color.TRANSPARENT));  
	myDialog.show();  
  
}

Let's add a new style, CustomDialog, in styles.xml.

    <style name="CustomDialog" parent="android:Theme.Dialog">  
        <item name="android:windowIsTranslucent">true</item>  
        <item name="android:windowBackground">@android:color/transparent</item>  
    </style>  
  
</resources>

Now, it's code to delete the FireMarker.

private void removeOnlineMarker() {
    ltag("Remove Online Marker: "+fireMarker.getTag());

    String uid = user.getUid().toString();
    DatabaseReference markerRef = tomtomRef.child("markers");
    DatabaseReference userRef =  markerRef.child(uid);
    DatabaseReference delRef = userRef.child(fireMarker.getTag());

    // Delete the firemarker  
    delRef.removeValue();
    fireMarker = null;
}

private void removeAllOnlineMarkers() {
    ltag("Remove All Online Markers");

    String uid = user.getUid().toString();
    DatabaseReference markerRef = tomtomRef.child("markers");
    DatabaseReference userRef =  markerRef.child(uid);
    // Delete the all markers of a user  
    userRef.removeValue();
    fireMarker = null;
}

It's removeValue(). Simple, isn't it? We also need to separate markers for each user. Let set boolLoadMarkers to be true in signInFirebase().

private void signInFirebase() {
    ltag("Sign in request");

    mAuth.signOut();
    boolLoadMarkers = true;

Run!

I create three markes by my Facebook account and save them to Firebase.







Looking good, one has removed. Next, remove them all.


Thanks for reading. Next part, I will code the address and bring the address into the markers.


My AD: If anyone likes my work, please send me an email, message, call or gift card (I am out of coffee). I am US Citizen. I guarantee your copyright and privacy is safe to work with me. I am out of job market right now. I will be glad to help with any projects, including your homework.

HomanHuang@gmail.com, 415-286-6020




To view or add a comment, sign in

More articles by Homan Huang

Others also viewed

Explore content categories