Beispiel #1
0
def get_finding_chart(source_ra,
                      source_dec,
                      source_name,
                      image_source='desi',
                      output_format='pdf',
                      imsize=3.0,
                      tick_offset=0.02,
                      tick_length=0.03,
                      fallback_image_source='dss',
                      **offset_star_kwargs):
    """Create a finder chart suitable for spectroscopic observations of
       the source

    Parameters
    ----------
    source_ra : float
        Right ascension (J2000) of the source
    source_dec : float
        Declination (J2000) of the source
    source_name : str
        Name of the source
    image_source : {'desi', 'dss', 'ztfref'}, optional
        Survey where the image comes from "desi", "dss", "ztfref"
        (more to be added)
    output_format : str, optional
        "pdf" of "png" -- determines the format of the returned finder
    imsize : float, optional
        Requested image size (on a size) in arcmin. Should be between 2-15.
    tick_offset : float, optional
        How far off the each source should the tick mark be made? (in arcsec)
    tick_length : float, optional
        How long should the tick mark be made? (in arcsec)
    fallback_image_source : str, optional
        Where what `image_source` should we fall back to if the
        one requested fails
    **offset_star_kwargs : dict, optional
        Other parameters passed to `get_nearby_offset_stars`

    Returns
    -------
    dict
        success : bool
            Whether the request was successful or not, returning
            a sensible error in 'reason'
        name : str
            suggested filename based on `source_name` and `output_format`
        data : str
            binary encoded data for the image (to be streamed)
        reason : str
            If not successful, a reason is returned.
    """
    if (imsize < 2.0) or (imsize > 15):
        return {
            'success': False,
            'reason': 'Requested `imsize` out of range',
            'data': '',
            'name': ''
        }

    if image_source not in source_image_parameters:
        return {
            'success': False,
            'reason': f'image source {image_source} not in list',
            'data': '',
            'name': ''
        }

    fig = plt.figure(figsize=(11, 8.5), constrained_layout=False)
    widths = [2.6, 1]
    heights = [2.6, 1]
    spec = fig.add_gridspec(ncols=2,
                            nrows=2,
                            width_ratios=widths,
                            height_ratios=heights,
                            left=0.05,
                            right=0.95)

    # how wide on the side will the image be? 256 as default
    npixels = source_image_parameters[image_source].get("npixels", 256)
    # set the pixelscale in arcsec (typically about 1 arcsec/pixel)
    pixscale = 60 * imsize / npixels

    hdu = fits_image(source_ra,
                     source_dec,
                     imsize=imsize,
                     image_source=image_source)

    # skeleton WCS - this is the field that the user requested
    wcs = WCS(naxis=2)

    # set the headers of the WCS.
    # The center of the image is the reference point (source_ra, source_dec):
    wcs.wcs.crpix = [npixels / 2, npixels / 2]
    wcs.wcs.crval = [source_ra, source_dec]

    # create the pixel scale and orientation North up, East left
    # pixelscale is in degrees, established in the tangent plane
    # to the reference point
    wcs.wcs.cd = np.array([[-pixscale / 3600, 0], [0, pixscale / 3600]])
    wcs.wcs.ctype = ["RA---TAN", "DEC--TAN"]

    if hdu is not None:
        im = hdu.data

        # replace the nans with medians
        im[np.isnan(im)] = np.nanmedian(im)
        if source_image_parameters[image_source].get("reproject", False):
            # project image to the skeleton WCS solution
            print("Reprojecting image to requested position and orientation")
            im, _ = reproject_adaptive(hdu, wcs, shape_out=(npixels, npixels))
        else:
            wcs = WCS(hdu.header)

        if source_image_parameters[image_source].get("smooth", False):
            im = gaussian_filter(
                hdu.data,
                source_image_parameters[image_source]["smooth"] / pixscale)

        norm = ImageNormalize(im, interval=ZScaleInterval())
        watermark = source_image_parameters[image_source]["str"]

    else:
        # if we got back a blank image, try to fallback on another survey
        # and return the results from that call
        if (fallback_image_source is not None):
            if (fallback_image_source != image_source):
                print(f"Falling back on image source {fallback_image_source}")
                return get_finding_chart(source_ra,
                                         source_dec,
                                         source_name,
                                         image_source=fallback_image_source,
                                         output_format=output_format,
                                         imsize=imsize,
                                         tick_offset=tick_offset,
                                         tick_length=tick_length,
                                         fallback_image_source=None,
                                         **offset_star_kwargs)

        # we dont have an image here, so let's create a dummy one
        # so we can still plot
        im = np.zeros((npixels, npixels))
        norm = None
        watermark = None

    # add the images in the top left corner
    ax = fig.add_subplot(spec[0, 0], projection=wcs)
    ax_text = fig.add_subplot(spec[0, 1])
    ax_text.axis('off')
    ax_starlist = fig.add_subplot(spec[1, 0:])
    ax_starlist.axis('off')

    ax.imshow(im, origin='lower', norm=norm, cmap='gray_r')
    ax.set_autoscale_on(False)
    ax.grid(color='white', ls='dotted')
    ax.set_xlabel(r'$\alpha$ (J2000)', fontsize='large')
    ax.set_ylabel(r'$\delta$ (J2000)', fontsize='large')
    obstime = offset_star_kwargs.get("obstime",
                                     datetime.datetime.utcnow().isoformat())
    ax.set_title(f'{source_name} Finder ({obstime})',
                 fontsize='large',
                 fontweight='bold')

    star_list, _, _, _ = get_nearby_offset_stars(source_ra, source_dec,
                                                 source_name,
                                                 **offset_star_kwargs)

    if not isinstance(star_list, list) or len(star_list) == 0:
        return {
            'success': False,
            'reason': 'failure to get star list',
            'data': '',
            'name': ''
        }

    ncolors = len(star_list)
    colors = sns.color_palette("colorblind", ncolors)

    start_text = [-0.35, 0.99]
    starlist_str = (
        "# Note: spacing in starlist many not copy/paste correctly in PDF\n" +
        f"#       you can get starlist directly from" +
        f" /api/sources/{source_name}/offsets?" +
        f"facility={offset_star_kwargs.get('facility', 'Keck')}\n" +
        "\n".join([x["str"] for x in star_list]))

    # add the starlist
    ax_starlist.text(0,
                     0.50,
                     starlist_str,
                     fontsize="x-small",
                     family='monospace',
                     transform=ax_starlist.transAxes)

    # add the watermark for the survey
    props = dict(boxstyle='round', facecolor='gray', alpha=0.5)

    if watermark is not None:
        ax.text(0.035,
                0.035,
                watermark,
                horizontalalignment='left',
                verticalalignment='center',
                transform=ax.transAxes,
                fontsize='medium',
                fontweight='bold',
                color="yellow",
                alpha=0.5,
                bbox=props)

    ax.text(
        0.95,
        0.035,
        f"{imsize}\u2032 \u00D7 {imsize}\u2032",  # size'x size'
        horizontalalignment='right',
        verticalalignment='center',
        transform=ax.transAxes,
        fontsize='medium',
        fontweight='bold',
        color="yellow",
        alpha=0.5,
        bbox=props)

    # compass rose
    # rose_center_pixel = ax.transAxes.transform((0.04, 0.95))
    rose_center = pixel_to_skycoord(int(npixels * 0.1), int(npixels * 0.9),
                                    wcs)
    props = dict(boxstyle='round', facecolor='gray', alpha=0.5)

    for ang, label, off in [(0, "N", 0.01), (90, "E", 0.03)]:
        position_angle = ang * u.deg
        separation = (0.05 * imsize * 60) * u.arcsec  # 5%
        p2 = rose_center.directional_offset_by(position_angle, separation)
        ax.plot([rose_center.ra.value, p2.ra.value],
                [rose_center.dec.value, p2.dec.value],
                transform=ax.get_transform('world'),
                color="gold",
                linewidth=2)

        # label N and E
        position_angle = (ang + 15) * u.deg
        separation = ((0.05 + off) * imsize * 60) * u.arcsec
        p2 = rose_center.directional_offset_by(position_angle, separation)
        ax.text(p2.ra.value,
                p2.dec.value,
                label,
                color="gold",
                transform=ax.get_transform('world'),
                fontsize='large',
                fontweight='bold')

    for i, star in enumerate(star_list):

        c1 = SkyCoord(star["ra"] * u.deg, star["dec"] * u.deg, frame='icrs')

        # mark up the right side of the page with position and offset info
        name_title = star["name"]
        if star.get("mag") is not None:
            name_title += f", mag={star.get('mag'):.2f}"
        ax_text.text(start_text[0],
                     start_text[1] - i / ncolors,
                     name_title,
                     ha='left',
                     va='top',
                     fontsize='large',
                     fontweight='bold',
                     transform=ax_text.transAxes,
                     color=colors[i])
        source_text = f"  {star['ra']:.5f} {star['dec']:.5f}\n"
        source_text += f"  {c1.to_string('hmsdms')}\n"
        if (star.get("dras") is not None) and (star.get("ddecs") is not None):
            source_text += \
               f'  {star.get("dras")} {star.get("ddecs")} to {source_name}'
        ax_text.text(start_text[0],
                     start_text[1] - i / ncolors - 0.06,
                     source_text,
                     ha='left',
                     va='top',
                     fontsize='large',
                     transform=ax_text.transAxes,
                     color=colors[i])

        # work on making marks where the stars are
        for ang in [0, 90]:
            position_angle = ang * u.deg
            separation = (tick_offset * imsize * 60) * u.arcsec
            p1 = c1.directional_offset_by(position_angle, separation)
            separation = (tick_offset + tick_length) * imsize * 60 * u.arcsec
            p2 = c1.directional_offset_by(position_angle, separation)
            ax.plot([p1.ra.value, p2.ra.value], [p1.dec.value, p2.dec.value],
                    transform=ax.get_transform('world'),
                    color=colors[i],
                    linewidth=3 if imsize <= 4 else 2)
        if star["name"].find("_off") != -1:
            # this is an offset star
            text = star["name"].split("_off")[-1]
            position_angle = 14 * u.deg
            separation = \
                (tick_offset + tick_length*1.6) * imsize * 60 * u.arcsec
            p1 = c1.directional_offset_by(position_angle, separation)
            ax.text(p1.ra.value,
                    p1.dec.value,
                    text,
                    color=colors[i],
                    transform=ax.get_transform('world'),
                    fontsize='large',
                    fontweight='bold')

    buf = io.BytesIO()
    fig.savefig(buf, format=output_format)
    buf.seek(0)

    return {
        "success": True,
        "name": f"finder_{source_name}.{output_format}",
        "data": buf.read(),
        "reason": ""
    }
Beispiel #2
0
def get_finding_chart(
    source_ra,
    source_dec,
    source_name,
    image_source='ps1',
    output_format='pdf',
    imsize=3.0,
    tick_offset=0.02,
    tick_length=0.03,
    fallback_image_source='dss',
    zscale_contrast=0.045,
    zscale_krej=2.5,
    extra_display_string="",
    **offset_star_kwargs,
):
    """Create a finder chart suitable for spectroscopic observations of
       the source

    Parameters
    ----------
    source_ra : float
        Right ascension (J2000) of the source
    source_dec : float
        Declination (J2000) of the source
    source_name : str
        Name of the source
    image_source : {'desi', 'dss', 'ztfref', 'ps1'}, optional
        Survey where the image comes from "desi", "dss", "ztfref", "ps1"
        defaults to "ps1"
    output_format : str, optional
        "pdf" of "png" -- determines the format of the returned finder
    imsize : float, optional
        Requested image size (on a size) in arcmin. Should be between 2-15.
    tick_offset : float, optional
        How far off the each source should the tick mark be made? (in arcsec)
    tick_length : float, optional
        How long should the tick mark be made? (in arcsec)
    fallback_image_source : str, optional
        Where what `image_source` should we fall back to if the
        one requested fails
    zscale_contrast : float, optional
        Contrast parameter for the ZScale interval
    zscale_krej : float, optional
        Krej parameter for the Zscale interval
    extra_display_string :  str, optional
        What else to show for the source itself in the chart (e.g. proper motion)
    **offset_star_kwargs : dict, optional
        Other parameters passed to `get_nearby_offset_stars`

    Returns
    -------
    dict
        success : bool
            Whether the request was successful or not, returning
            a sensible error in 'reason'
        name : str
            suggested filename based on `source_name` and `output_format`
        data : str
            binary encoded data for the image (to be streamed)
        reason : str
            If not successful, a reason is returned.
    """
    if (imsize < 2.0) or (imsize > 15):
        return {
            'success': False,
            'reason': 'Requested `imsize` out of range',
            'data': '',
            'name': '',
        }

    if image_source not in source_image_parameters:
        return {
            'success': False,
            'reason': f'image source {image_source} not in list',
            'data': '',
            'name': '',
        }

    matplotlib.use("Agg")
    fig = plt.figure(figsize=(11, 8.5), constrained_layout=False)
    widths = [2.6, 1]
    heights = [2.6, 1]
    spec = fig.add_gridspec(
        ncols=2,
        nrows=2,
        width_ratios=widths,
        height_ratios=heights,
        left=0.05,
        right=0.95,
    )

    # how wide on the side will the image be? 256 as default
    npixels = source_image_parameters[image_source].get("npixels", 256)
    # set the pixelscale in arcsec (typically about 1 arcsec/pixel)
    pixscale = 60 * imsize / npixels

    hdu = fits_image(source_ra,
                     source_dec,
                     imsize=imsize,
                     image_source=image_source)

    # skeleton WCS - this is the field that the user requested
    wcs = WCS(naxis=2)

    # set the headers of the WCS.
    # The center of the image is the reference point (source_ra, source_dec):
    wcs.wcs.crpix = [npixels / 2, npixels / 2]
    wcs.wcs.crval = [source_ra, source_dec]

    # create the pixel scale and orientation North up, East left
    # pixelscale is in degrees, established in the tangent plane
    # to the reference point
    wcs.wcs.cd = np.array([[-pixscale / 3600, 0], [0, pixscale / 3600]])
    wcs.wcs.ctype = ["RA---TAN", "DEC--TAN"]

    fallback = True
    if hdu is not None:
        im = hdu.data

        # replace the nans with medians
        im[np.isnan(im)] = np.nanmedian(im)

        # Fix the header keyword for the input system, if needed
        hdr = hdu.header
        if 'RADECSYS' in hdr:
            hdr.set('RADESYSa', hdr['RADECSYS'], before='RADECSYS')
            del hdr['RADECSYS']

        if source_image_parameters[image_source].get("reproject", False):
            # project image to the skeleton WCS solution
            log("Reprojecting image to requested position and orientation")
            im, _ = reproject_adaptive(hdu, wcs, shape_out=(npixels, npixels))
        else:
            wcs = WCS(hdu.header)

        if source_image_parameters[image_source].get("smooth", False):
            im = gaussian_filter(
                hdu.data,
                source_image_parameters[image_source]["smooth"] / pixscale)

        cent = int(npixels / 2)
        width = int(0.05 * npixels)
        test_slice = slice(cent - width, cent + width)
        all_nans = np.isnan(im[test_slice, test_slice].flatten()).all()
        all_zeros = (im[test_slice, test_slice].flatten() == 0).all()
        if not (all_zeros or all_nans):
            percents = np.nanpercentile(im.flatten(), [10, 99.0])
            vmin = percents[0]
            vmax = percents[1]
            interval = ZScaleInterval(
                nsamples=int(0.1 * (im.shape[0] * im.shape[1])),
                contrast=zscale_contrast,
                krej=zscale_krej,
            )
            norm = ImageNormalize(im, vmin=vmin, vmax=vmax, interval=interval)
            watermark = source_image_parameters[image_source]["str"]
            fallback = False

    if hdu is None or fallback:
        # if we got back a blank image, try to fallback on another survey
        # and return the results from that call
        if fallback_image_source is not None:
            if fallback_image_source != image_source:
                log(f"Falling back on image source {fallback_image_source}")
                return get_finding_chart(
                    source_ra,
                    source_dec,
                    source_name,
                    image_source=fallback_image_source,
                    output_format=output_format,
                    imsize=imsize,
                    tick_offset=tick_offset,
                    tick_length=tick_length,
                    fallback_image_source=None,
                    **offset_star_kwargs,
                )

        # we dont have an image here, so let's create a dummy one
        # so we can still plot
        im = np.zeros((npixels, npixels))
        watermark = None
        vmin = 0
        vmax = 0
        norm = ImageNormalize(im, vmin=vmin, vmax=vmax)

    # add the images in the top left corner
    ax = fig.add_subplot(spec[0, 0], projection=wcs)
    ax_text = fig.add_subplot(spec[0, 1])
    ax_text.axis('off')
    ax_starlist = fig.add_subplot(spec[1, 0:])
    ax_starlist.axis('off')

    ax.imshow(im, origin='lower', norm=norm, cmap='gray_r')
    ax.set_autoscale_on(False)
    ax.grid(color='white', ls='dotted')
    ax.set_xlabel(r'$\alpha$ (J2000)', fontsize='large')
    ax.set_ylabel(r'$\delta$ (J2000)', fontsize='large')
    obstime = offset_star_kwargs.get("obstime",
                                     datetime.datetime.utcnow().isoformat())
    ax.set_title(
        f'{source_name} Finder (for {obstime.split("T")[0]})',
        fontsize='large',
        fontweight='bold',
    )

    star_list, _, _, _, used_ztfref = get_nearby_offset_stars(
        source_ra, source_dec, source_name, **offset_star_kwargs)

    if not isinstance(star_list, list) or len(star_list) == 0:
        return {
            'success': False,
            'reason': 'failure to get star list',
            'data': '',
            'name': '',
        }

    ncolors = len(star_list)
    if star_list[0]['str'].startswith("!Data"):
        ncolors -= 1
    colors = sns.color_palette("colorblind", ncolors)

    start_text = [-0.45, 0.99]
    origin = "GaiaEDR3" if not used_ztfref else "ZTFref"
    starlist_str = (
        f"# Note: {origin} used for offset star positions\n"
        "# Note: spacing in starlist many not copy/paste correctly in PDF\n" +
        "#       you can get starlist directly from" +
        f" /api/sources/{source_name}/offsets?" +
        f"facility={offset_star_kwargs.get('facility', 'Keck')}\n" +
        "\n".join([x["str"] for x in star_list]))

    # add the starlist
    ax_starlist.text(
        0,
        0.50,
        starlist_str,
        fontsize="x-small",
        family='monospace',
        transform=ax_starlist.transAxes,
    )

    # add the watermark for the survey
    props = dict(boxstyle='round', facecolor='gray', alpha=0.7)

    if watermark is not None:
        ax.text(
            0.035,
            0.035,
            watermark,
            horizontalalignment='left',
            verticalalignment='center',
            transform=ax.transAxes,
            fontsize='medium',
            fontweight='bold',
            color="yellow",
            alpha=0.5,
            bbox=props,
        )

    date_obs = hdr.get('DATE-OBS')
    if not date_obs:
        mjd_obs = hdr.get('MJD-OBS')
        if mjd_obs:
            date_obs = Time(f"{mjd_obs}",
                            format='mjd').to_value('fits', subfmt='date_hms')

    if date_obs:
        ax.text(
            0.95,
            0.95,
            f'image date {date_obs.split("T")[0]}',
            horizontalalignment='right',
            verticalalignment='center',
            transform=ax.transAxes,
            fontsize='small',
            color="yellow",
            alpha=0.5,
            bbox=props,
        )

    ax.text(
        0.95,
        0.035,
        f"{imsize}\u2032 \u00D7 {imsize}\u2032",  # size'x size'
        horizontalalignment='right',
        verticalalignment='center',
        transform=ax.transAxes,
        fontsize='medium',
        fontweight='bold',
        color="yellow",
        alpha=0.5,
        bbox=props,
    )

    # compass rose
    # rose_center_pixel = ax.transAxes.transform((0.04, 0.95))
    rose_center = pixel_to_skycoord(int(npixels * 0.1), int(npixels * 0.9),
                                    wcs)
    props = dict(boxstyle='round', facecolor='gray', alpha=0.5)

    for ang, label, off in [(0, "N", 0.01), (90, "E", 0.03)]:
        position_angle = ang * u.deg
        separation = (0.05 * imsize * 60) * u.arcsec  # 5%
        p2 = rose_center.directional_offset_by(position_angle, separation)
        ax.plot(
            [rose_center.ra.value, p2.ra.value],
            [rose_center.dec.value, p2.dec.value],
            transform=ax.get_transform('world'),
            color="gold",
            linewidth=2,
        )

        # label N and E
        position_angle = (ang + 15) * u.deg
        separation = ((0.05 + off) * imsize * 60) * u.arcsec
        p2 = rose_center.directional_offset_by(position_angle, separation)
        ax.text(
            p2.ra.value,
            p2.dec.value,
            label,
            color="gold",
            transform=ax.get_transform('world'),
            fontsize='large',
            fontweight='bold',
        )

    # account for Shane header
    if star_list[0]['str'].startswith("!Data"):
        star_list = star_list[1:]

    for i, star in enumerate(star_list):

        c1 = SkyCoord(star["ra"] * u.deg, star["dec"] * u.deg, frame='icrs')

        # mark up the right side of the page with position and offset info
        name_title = star["name"]
        if star.get("mag") is not None:
            name_title += f" {star.get('mag'):.2f} mag"
        ax_text.text(
            start_text[0],
            start_text[1] - (i * 1.1) / ncolors,
            name_title,
            ha='left',
            va='top',
            fontsize='large',
            fontweight='bold',
            transform=ax_text.transAxes,
            color=colors[i],
        )
        source_text = f"  {star['ra']:.5f} {star['dec']:.5f}\n"
        source_text += f"  {c1.to_string('hmsdms', precision=2)}\n"
        if i == 0 and extra_display_string != "":
            source_text += f"  {extra_display_string}\n"
        if ((star.get("dras") is not None) and (star.get("ddecs") is not None)
                and (star.get("pa") is not None)):
            source_text += f'  {star.get("dras")} {star.get("ddecs")} (PA={star.get("pa"):<0.02f}°)'
        ax_text.text(
            start_text[0],
            start_text[1] - (i * 1.1) / ncolors - 0.06,
            source_text,
            ha='left',
            va='top',
            fontsize='large',
            transform=ax_text.transAxes,
            color=colors[i],
        )

        # work on making marks where the stars are
        for ang in [0, 90]:
            # for the source itself (i=0), change the angle of the lines in
            # case the offset star is the same as the source itself
            position_angle = ang * u.deg if i != 0 else (ang + 225) * u.deg
            separation = (tick_offset * imsize * 60) * u.arcsec
            p1 = c1.directional_offset_by(position_angle, separation)
            separation = (tick_offset + tick_length) * imsize * 60 * u.arcsec
            p2 = c1.directional_offset_by(position_angle, separation)
            ax.plot(
                [p1.ra.value, p2.ra.value],
                [p1.dec.value, p2.dec.value],
                transform=ax.get_transform('world'),
                color=colors[i],
                linewidth=3 if imsize <= 4 else 2,
                alpha=0.8,
            )
        if star["name"].find("_o") != -1:
            # this is an offset star
            text = star["name"].split("_o")[-1]
            position_angle = 14 * u.deg
            separation = (tick_offset +
                          tick_length * 1.6) * imsize * 60 * u.arcsec
            p1 = c1.directional_offset_by(position_angle, separation)
            ax.text(
                p1.ra.value,
                p1.dec.value,
                text,
                color=colors[i],
                transform=ax.get_transform('world'),
                fontsize='large',
                fontweight='bold',
            )

    buf = io.BytesIO()
    fig.savefig(buf, format=output_format)
    plt.close(fig)
    buf.seek(0)

    return {
        "success": True,
        "name": f"finder_{source_name}.{output_format}",
        "data": buf.read(),
        "reason": "",
    }
Beispiel #3
0
    def reproject_to(self,
                     target_wcs,
                     algorithm='interpolation',
                     shape_out=None,
                     order='bilinear',
                     output_array=None,
                     parallel=False,
                     return_footprint=False):
        """
        Reprojects this NDCube to the coordinates described by another WCS object.

        Parameters
        ----------
        algorithm: `str`
            The algorithm to use for reprojecting. This can be any of: 'interpolation', 'adaptive',
            and 'exact'.

        target_wcs : `astropy.wcs.wcsapi.BaseHighLevelWCS`, `astropy.wcs.wcsapi.BaseLowLevelWCS`,
            or `astropy.io.fits.Header`
            The WCS object to which the ``NDCube`` is to be reprojected.

        shape_out: `tuple`, optional
            The shape of the output data array. The ordering of the dimensions must follow NumPy
            ordering and not the WCS pixel shape.
            If not specified, `~astropy.wcs.wcsapi.BaseLowLevelWCS.array_shape` attribute
            (if available) from the low level API of the ``target_wcs`` is used.

        order: `int` or `str`
            The order of the interpolation (used only when the 'interpolation' or 'adaptive'
            algorithm is selected).
            For 'interpolation' algorithm, this can be any of: 'nearest-neighbor', 'bilinear',
            'biquadratic', and 'bicubic'.
            For 'adaptive' algorithm, this can be either 'nearest-neighbor' or 'bilinear'.

        output_array: `numpy.ndarray`, optional
            An array in which to store the reprojected data. This can be any numpy array
            including a memory map, which may be helpful when dealing with extremely large files.

        parallel: `bool` or `int`
            Flag for parallel implementation (used only when the 'exact' algorithm is selected).
            If ``True``, a parallel implementation is chosen and the number of processes is
            selected automatically as the number of logical CPUs detected on the machine.
            If ``False``, a serial implementation is chosen.
            If the flag is a positive integer n greater than one, a parallel implementation
            using n processes is chosen.

        return_footprint: `bool`
            Whether to return the footprint in addition to the output NDCube.

        Returns
        -------
        resampled_cube : `ndcube.NDCube`
            A new resultant NDCube object, the supplied ``target_wcs`` will be the ``.wcs`` attribute of the output ``NDCube``.

        footprint: `numpy.ndarray`
            Footprint of the input array in the output array. Values of 0 indicate no coverage or
            valid values in the input image, while values of 1 indicate valid values.

        Notes
        -----
        This method doesn't support handling of the ``mask``, ``extra_coords``, and ``uncertainty`` attributes yet.
        However, ``meta`` and ``global_coords`` are copied to the output ``NDCube``.
        """
        try:
            from reproject import reproject_adaptive, reproject_exact, reproject_interp
            from reproject.wcs_utils import has_celestial
        except ModuleNotFoundError:
            raise ImportError(
                "The NDCube.reproject_to method requires the optional package `reproject`."
            )

        if isinstance(target_wcs, Mapping):
            target_wcs = WCS(header=target_wcs)

        low_level_target_wcs = utils.wcs.get_low_level_wcs(
            target_wcs, 'target_wcs')

        # 'adaptive' and 'exact' algorithms work only on 2D celestial WCS.
        if algorithm == 'adaptive' or algorithm == 'exact':
            if low_level_target_wcs.pixel_n_dim != 2 or low_level_target_wcs.world_n_dim != 2:
                raise ValueError(
                    'For adaptive and exact algorithms, target_wcs must be 2D.'
                )

            if not has_celestial(target_wcs):
                raise ValueError(
                    'For adaptive and exact algorithms, '
                    'target_wcs must contain celestial axes only.')

        if not utils.wcs.compare_wcs_physical_types(self.wcs, target_wcs):
            raise ValueError(
                'Given target_wcs is not compatible with this NDCube, the physical types do not match.'
            )

        # If shape_out is not specified explicity, try to extract it from the low level WCS
        if not shape_out:
            if hasattr(low_level_target_wcs, 'array_shape'
                       ) and low_level_target_wcs.array_shape is not None:
                shape_out = low_level_target_wcs.array_shape
            else:
                raise ValueError(
                    "shape_out must be specified if target_wcs does not have the array_shape attribute."
                )

        self._validate_algorithm_and_order(algorithm, order)

        if algorithm == 'interpolation':
            data = reproject_interp(self,
                                    output_projection=target_wcs,
                                    shape_out=shape_out,
                                    order=order,
                                    output_array=output_array,
                                    return_footprint=return_footprint)

        elif algorithm == 'adaptive':
            data = reproject_adaptive(self,
                                      output_projection=target_wcs,
                                      shape_out=shape_out,
                                      order=order,
                                      return_footprint=return_footprint)

        elif algorithm == 'exact':
            data = reproject_exact(self,
                                   output_projection=target_wcs,
                                   shape_out=shape_out,
                                   parallel=parallel,
                                   return_footprint=return_footprint)

        if return_footprint:
            data, footprint = data

        resampled_cube = type(self)(data,
                                    wcs=target_wcs,
                                    meta=deepcopy(self.meta))
        resampled_cube._global_coords = deepcopy(self.global_coords)

        if return_footprint:
            return resampled_cube, footprint

        return resampled_cube