Archives For November 2017

pulling away from apple

November 26, 2017

Two years ago around this date I wrote “all in with apple – part 1” in which I extolled the wondrous virtues of buying and using Apple products, from the iPhone and iPad through the Apple Watch and various Apple Macs, such as the Mac Mini and the Macbook Pro. By November 2015 I’d made the ultimate Apple purchase with an iPhone 6s Plus using Apple’s Upgrade Program. I’d use it again to update to the iPhone 7 Plus November 2016. That was the one act that began to push me away from seeing Apple through such industrial-grade rose colored glasses.

What pushed me farther along the path of dissatisfaction is the growing realization of how much money I’ve invested in Apple gear, such as in the aforementioned Macbook Pro and Apple Watch (the original as well as the Series 2) as well as other “sundry” Apple items. One item in particular was a 2015 13″ Macbook Air I purchased late November 2016 (right before Thanksgiving of that year and a trip to Japan) for my wife, which with very little effort was turned into a most expensive boat anchor.

Earlier this year my wife spilled coffee on her Air. The Mac still starts up, but there’s a problem with the display. Trying to get it fixed by Apple when it occured would have cost me at least $750. Of course, the first thought through my mind is how a very expensive computer doesn’t come with a little bit of moisture sealing, especially around the keyboard, to avoid such issues. But that’s the same question I always ask every time I see a $1,000+ camera or cell phone that isn’t environmentally sealed. How could you spend that much money on something that isn’t reasonably rugged? But we did, and suffered major damage due to a lack of rugged sealing on the Air.

And then there was the update to the iPhone 7 Plus. The 3.5″ audio plug was removed because of an arbitrary design decision on the part of Apple, requiring a dongle to be able to use my ear buds. The idea of spending another $100 to $200 for Bluetooth ear buds is ridiculous. With Air Pods I’ve got one more expensive item that needs constant charging, and the Air Pods are themselves too easy to loose. I now have first hand experience as to what can happen when someone is “brave” enough to remove a fundamentally key feature that should have never been removed in the first place.

To add insult to injury, Apple sold me the AT&T version 7 Plus, which has the Intel wireless modem in it. That meant not only a lack of performance compared to the Qualcomm modem, but an iPhone that effectively wouldn’t work overseas in either Japan or Korea. I solved the Korea problem in early 2017 by purchasing a factory unlocked Moto G4 Plus from B&H Photo for about $130. It came with Android 6, and updated OTA to Android 7 before I left to go to South Korea in March. Because it was unlocked I was able to purchase a KT SIM at the Inchon International Airport for the phone with unlimited voice and data for the two weeks I was there. That was an additional US$35.

The G4 Plus again proved its worth during Hurricane Irma. Moto phones (and the majority of Android phones in general) come with a built-in FM broadcast radio. The Moto has no SIM for local use, but I was still able to listen to the local FM radio stations for general emergency information as the storm passed overhead and knocked out all our power, including power to the area’s wireless cell towers, rendering my shiny iPhone 7 Plus all but useless. Fortunately for us the power came back on in less than 24 hours, including the wireless infrastructure. Only then was the iPhone 7 of any use for anything, especially communications.

And that $750 that was supposed to fix the Air? I spent about $600 of that on three (yes, three) Asus Chromebook 14s, each equipped with 4GB of DRAM, 32GB of internal storage, and a quad-core Intel Celeron. That was a sale price at a local Costco store. Two went to my wife (one to use and one as a backup), who’s repeatedly stated how there’s little difference between her $200 Chromebook and what she was able to do on the Air. And that includes the keyboard on the Chromebook, which is of surprisingly high quality. Even the Chromebook shell is nice, built of aluminum. If I do get the Air fixed, I’m more than inclined to do it myself using iFixit as a guide. Oh, and that third Chromebook? It’s going to be a Christmas gift.

Today’s technology in general, and Apple’s in particular, has been marketed as being something shiny and to be coveted. I’m here to tell you that the shine is definitely off and I no longer covet these very expensive baubles. I saw little reason to upgrade the iPhone 7 to an equivalent 8, and I have absolutely no use for the X. I’ve handled the X now that demo units are in the various stores, and I’m here to tell you that this isn’t a future I want to be a part of. I’ve read the glowing reviews about the X, and I’ve come to the conclusion they’re either delusional or they’ve been bought off and lying through their teeth. My wife and I are going to hang onto our 7s for as long as we possibly can. When it comes time for a replacement I’ll see what’s in the market.

For the foreseeable future (meaning years in my case) I’m done buying Apple or recommending Apple. If anything, buy practically only if you really need it and it doesn’t impact a budget. Apple doesn’t fit there.

NOTE: Apple icon by Dave Gandy: https://www.flaticon.com/free-icon/apple-logo_25345

As I continued to work a bit with the example application from yesterday, I started adding more code to the ResizeableCavas class so it would draw more stuff. In particular I wanted a grid drawn underneath the target as shown above. That’s when the aversion to too many lines of code in a given method or class kicked in, and I reorganized the application to break up functionality into something more manageable and reusable.

Thus, the creation of a CanvasLayer interface and two implementations using the interface, and some simple extensions to add those layers to the ResizeableCanvas. New code to follow.

package borderpaneexample;

import javafx.scene.canvas.GraphicsContext;

/**
 *
 * @author wbeebe
 */
public interface CanvasLayer {
    void draw(GraphicsContext gc, double width, double height);
}

And the two implementations.

package borderpaneexample;

import javafx.scene.canvas.GraphicsContext;
import javafx.scene.paint.Color;

/**
 *
 * @author wbeebe
 */
public class GridLayer implements CanvasLayer {

    @Override
    public void draw(GraphicsContext gc, double width, double height) {
        gc.clearRect(0, 0, width, height);
        // lighter than LIGHTGRAY
        gc.setStroke(Color.rgb(230, 230, 230));

        // Create all the vertical lines.
        //
        for (double i = 0; i <= width; i += 10) {
            gc.setLineWidth((i % 100) == 0 ? 2 : 1);
            gc.strokeLine(i, 0, i, height-2);
        }

        // Create all the horizontal lines.
        //
        for (double j = 0; j <= height; j += 10) {
            gc.setLineWidth((j % 100) == 0 ? 2 : 1);
            gc.strokeLine(0, j, width-2, j);
        }
    }
}
package borderpaneexample;

import javafx.scene.canvas.GraphicsContext;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;

/**
 *
 * @author wbeebe
 */
public class TargetLayer implements CanvasLayer {

    @Override
    public void draw(GraphicsContext gc, double width, double height) {
        gc.fillOval((width - 40)/2.0, (height - 40)/2.0, 40, 40);

        Font font = gc.getFont();
        gc.fillText("Width: " + Double.toString(width), width/2 + 2, font.getSize());

        // Some interesting graphics context manipulation. Move to the left
        // edge and print the height, rotated 90 degrees parallel to the
        // left edge.
        //
        gc.save();
        gc.translate(font.getSize(), height/2);
        gc.rotate(-90);
        gc.fillText("Height: " + Double.toString(height), 2, 0);
        gc.restore();

        gc.setStroke(Color.RED);
        gc.setLineWidth(2);
        gc.strokeLine(0, 0, width, height);
        gc.strokeLine(0, height, width, 0);
        gc.strokeLine(width/2.0, 0, width/2.0, height);
        gc.strokeLine(0, height/2.0, width, height/2.0);
    }
}

And now the reworked ResizeableCanvas. A new method was implemented to add CanvasLayers to be drawn. All changes to support a list of CanvasLayers are highlighted.

package borderpaneexample;

import java.util.ArrayList;
import java.util.List;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.layout.Region;

/**
 *
 * @author wbeebe
 */

public class ResizeableCanvas extends Canvas {
    List<CanvasLayer>canvasLayerList;
    /**
     * ResizeableCanvas provides a resizeable Canvas.
     * 
     * Resizeable Canvas requires that it be wrapped in a Region.
 
 To use, follow these steps:
 1) Instantiate a wrap around Region instance, such as Pane;
 2) Instantiate a ResizeableCanvas with the Region class instance;
 3) Call the Region's getChildren().add(...) method to add the
    ResizeableCanvas instance to the Region instance.
 
 This is especially effective, and required, if you want a resizable
 Canvas in the center of a BorderPane and want to use any of the other
 BorderPane regions.
 
 Internally the Resizable Canvas is bound to the wrapper Region's
 width and heigh property; when the wrapper Region is resized, then
 the Resiable Canvas' resizeDraw() method is called, producing the effect of
 resizing through redrawing.
 
 For this to work, isResizable() must be overridden to return true,
 prefWidth() overridden to return the current width, and
 prefHeight() overridden to return the current height.
     * 
     * @param region 
     */
    public ResizeableCanvas(Region region) {
        this.canvasLayerList = new ArrayList<>();
        widthProperty().bind(region.widthProperty());
        heightProperty().bind(region.heightProperty());
        widthProperty().addListener(event -> resizeDraw() );
        heightProperty().addListener(event -> resizeDraw() );
    }

    /**
     * Basically shows how to use the GraphicsContext to resizeDraw into the Canvas.
     * Any graphic operation that can be supported by a Canvas can be performed
     * here on a resize event.
     */
    private void resizeDraw() {
        double width = getWidth();
        double height = getHeight();
        GraphicsContext gc = getGraphicsContext2D();

        canvasLayerList.forEach((canvasLayer) -> {
            canvasLayer.draw(gc, width, height);
        });
    }

    public void addLayer(CanvasLayer canvasLayer) {
        canvasLayerList.add(canvasLayer);
    }

    @Override
    public boolean isResizable() {
      return true;
    }
 
    @Override
    public double prefWidth(double height) {
      return getWidth();
    }
 
    @Override
    public double prefHeight(double width) {
      return getHeight();
    }
}

And finally the main class. Again, code for implementing the ResizeableCanvas and its changes are highlighted.

package borderpaneexample;

import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

/**
 *
 * @author wbeebe
 */
public class BorderPaneExample extends Application {
    
    @Override
    public void start(Stage primaryStage) {
        BorderPane borderPane = new BorderPane();

        Button rightButton = new Button("Right");
        rightButton.setOnAction((ActionEvent event) -> {
            System.out.println("Right button");
        });

        Button leftButton = new Button("Left");
        leftButton.setOnAction((ActionEvent event) -> {
            System.out.println("Left button");
        });

        Button topButton = new Button("Top");
        topButton.setOnAction((ActionEvent event) -> {
            System.out.println("Top button");
        });

        Button bottomButton = new Button("Bottom");
        bottomButton.setOnAction((ActionEvent event) -> {
            System.out.println("Bottom button");
        });
        
        Pane centerPane = new Pane();
        ResizeableCanvas resizeableCanvas = new ResizeableCanvas(centerPane);
        centerPane.getChildren().add(resizeableCanvas);
        centerPane.setStyle(
            "-fx-padding: 0;" +
            "-fx-border-style: solid inside;" +
            "-fx-border-width: 1;" +
            "-fx-border-insets: 0;" +
            "-fx-border-radius: 0;" +
            "-fx-border-color: #000;");
        resizeableCanvas.addLayer(new GridLayer());
        resizeableCanvas.addLayer(new TargetLayer());

        borderPane.setCenter(centerPane);
        borderPane.setRight(rightButton);
        BorderPane.setAlignment(rightButton, Pos.CENTER_RIGHT);
        VBox vbox = makeVBox();
        vbox.getChildren().add(leftButton);
        borderPane.setLeft(vbox);
        BorderPane.setAlignment(leftButton, Pos.CENTER_LEFT);
        HBox hbox = makeHBox();
        hbox.getChildren().add(topButton);
        borderPane.setTop(hbox);
        BorderPane.setAlignment(topButton, Pos.TOP_CENTER);
        borderPane.setBottom(bottomButton);
        BorderPane.setAlignment(bottomButton, Pos.BOTTOM_CENTER);
        borderPane.setStyle(
            "-fx-padding: 2;" +
            "-fx-border-style: solid inside;" +
            "-fx-border-width: 2;" +
            "-fx-border-insets: 2;" +
            "-fx-border-radius: 0;" +
            "-fx-border-color: #cccccc;");


        Scene scene = new Scene(borderPane, 800, 600);
        primaryStage.setTitle("BorderPane Example with Canvas");
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        launch(args);
    }

    public HBox makeHBox() {
        HBox hbox = new HBox();
        hbox.setPadding(new Insets(5, 5, 5, 5));
        hbox.setSpacing(5);
        hbox.setStyle("-fx-background-color: #cccccc;");
        return hbox;
    }

    public VBox makeVBox() {
        VBox vbox = new VBox();
        vbox.setPadding(new Insets(5, 5, 5, 5));
        vbox.setSpacing(5);
        vbox.setStyle("-fx-background-color: #dddddd;");
        return vbox;
    }
}

The order that CanvasLayers are added is important, as that’s the order in which they are rendered on a call to ResizeableCanvas’ resizeDraw() method. It’s also important to realize that the lowest layer to render should have a call to GraphicsContext’s clearRect(…) method in order to clear the Canvas area before anything is redrawn. Else ResizeableCanvas rapidly turns into a mess every time the application is resized. Comment out the addition of GridLayer (line 55 above) so that only TargetLayer is added and watch what happens as you resize the application.

Not sure at this point what to do next. A map could easily be displayed in a layer, but ResizeableCanvas doesn’t know about a viewport into an arbitrarily larger context, or the ability to move around within it using sliders. Something else to contemplate.